diff --git a/test2/faces40/pom.xml b/test2/faces40/pom.xml
new file mode 100644
index 0000000000..3b50ab0a5c
--- /dev/null
+++ b/test2/faces40/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+ 4.0.0
+
+
+ org.glassfish.mojarra.test2
+ pom
+ 4.0.0-SNAPSHOT
+
+
+ org.glassfish.mojarra.faces40
+ pom
+ pom
+
+ Mojarra ${project.version} - Test - Faces 4.0
+
+
+ selectItemGroups
+
+
diff --git a/test2/faces40/selectItemGroups/pom.xml b/test2/faces40/selectItemGroups/pom.xml
new file mode 100644
index 0000000000..85d45bbf27
--- /dev/null
+++ b/test2/faces40/selectItemGroups/pom.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ 4.0.0
+
+
+ org.glassfish.mojarra.faces40
+ pom
+ 4.0.0-SNAPSHOT
+
+
+ selectItemGroups
+ war
+
+ Mojarra ${project.version} - Test - Faces 4.0 - f:selectItemGroups
+
+
+ test-faces40-selectItemGroups
+
+
diff --git a/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Category.java b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Category.java
new file mode 100644
index 0000000000..1c1538e8a7
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Category.java
@@ -0,0 +1,34 @@
+package com.sun.faces.test.servlet50.selectitemgroups;
+
+import static java.util.Arrays.asList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Category {
+
+ private String name;
+ private List products = new ArrayList<>();
+
+ public Category() {
+ //
+ }
+
+ public Category(String name, Product... products) {
+ this.name = name;
+ this.products = asList(products);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public List getProducts() {
+ return products;
+ }
+
+}
\ No newline at end of file
diff --git a/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Product.java b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Product.java
new file mode 100644
index 0000000000..2fc859923a
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Product.java
@@ -0,0 +1,33 @@
+package com.sun.faces.test.servlet50.selectitemgroups;
+
+public class Product {
+
+ private Long id;
+ private String name;
+
+ public Product() {
+ //
+ }
+
+ public Product(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+}
\ No newline at end of file
diff --git a/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559ITBean.java b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559ITBean.java
new file mode 100644
index 0000000000..84f7b8b876
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/main/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559ITBean.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020 Contributors to 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
+ * http://www.eclipse.org/legal/epl-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: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package com.sun.faces.test.servlet50.selectitemgroups;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Named;
+
+@Named
+@RequestScoped
+public class Spec1559ITBean {
+
+ private List categories = new ArrayList<>();
+ private Long selectedProductId;
+
+ @PostConstruct
+ public void init() {
+ categories.add(new Category("Animals", new Product(1L, "Dog"), new Product(2L, "Cat"), new Product(3L, "Fish")));
+ categories.add(new Category("Cars", new Product(4L, "Alfa"), new Product(5L, "Audi"), new Product(6L, "BMW")));
+ }
+
+ public List getCategories() {
+ return categories;
+ }
+
+ public Long getSelectedProductId() {
+ return selectedProductId;
+ }
+
+ public void setSelectedProductId(Long selectedProductId) {
+ this.selectedProductId = selectedProductId;
+ }
+}
diff --git a/test2/faces40/selectItemGroups/src/main/webapp/WEB-INF/web.xml b/test2/faces40/selectItemGroups/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..683920c964
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ jakarta.faces.PROJECT_STAGE
+ ${webapp.projectStage}
+
+
+ jakarta.faces.PARTIAL_STATE_SAVING
+ ${webapp.partialStateSaving}
+
+
+ jakarta.faces.STATE_SAVING_METHOD
+ ${webapp.stateSavingMethod}
+
+
+ jakarta.faces.SERIALIZE_SERVER_STATE
+ ${webapp.serializeServerState}
+
+
+
+ facesServlet
+ jakarta.faces.webapp.FacesServlet
+
+
+ facesServlet
+ *.xhtml
+
+
+
+ spec1559IT.xhtml
+
+
diff --git a/test2/faces40/selectItemGroups/src/main/webapp/spec1559IT.xhtml b/test2/faces40/selectItemGroups/src/main/webapp/spec1559IT.xhtml
new file mode 100644
index 0000000000..759a5078f6
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/main/webapp/spec1559IT.xhtml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test2/faces40/selectItemGroups/src/test/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559IT.java b/test2/faces40/selectItemGroups/src/test/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559IT.java
new file mode 100644
index 0000000000..32de2b3615
--- /dev/null
+++ b/test2/faces40/selectItemGroups/src/test/java/com/sun/faces/test/servlet50/selectitemgroups/Spec1559IT.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020 Contributors to 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
+ * http://www.eclipse.org/legal/epl-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: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package com.sun.faces.test.servlet50.selectitemgroups;
+
+import static java.lang.System.getProperty;
+import static org.jboss.shrinkwrap.api.ShrinkWrap.create;
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+import java.net.URL;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.importer.ZipImporter;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.DomElement;
+import com.gargoylesoftware.htmlunit.html.HtmlOption;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlSelect;
+
+@RunWith(Arquillian.class)
+public class Spec1559IT {
+
+ @ArquillianResource
+ private URL webUrl;
+ private WebClient webClient;
+
+ @Deployment(testable = false)
+ public static WebArchive createDeployment() {
+ return create(ZipImporter.class, getProperty("finalName") + ".war")
+ .importFrom(new File("target/" + getProperty("finalName") + ".war"))
+ .as(WebArchive.class);
+ }
+
+ @Before
+ public void setUp() {
+ webClient = new WebClient();
+ }
+
+ @After
+ public void tearDown() {
+ webClient.close();
+ }
+
+ @Test
+ public void test() throws Exception {
+ HtmlPage page = webClient.getPage(webUrl + "spec1559IT.xhtml");
+ HtmlSelect select = page.getHtmlElementById("form:input");
+
+ assertValidMarkup(select);
+
+ select.setSelectedAttribute(select.getOptionByValue("5"), true);
+
+ assertEquals("messages is empty before submit", "", page.getHtmlElementById("form:messages").asText());
+ assertEquals("output is empty before submit", "", page.getHtmlElementById("form:output").asText());
+
+ page = page.getHtmlElementById("form:submit").click();
+
+ assertValidMarkup(select);
+ assertEquals("messages is still empty after submit", "", page.getHtmlElementById("form:messages").asText());
+ assertEquals("output is '5' after submit", "5", page.getHtmlElementById("form:output").asText());
+ }
+
+ private static void assertValidMarkup(HtmlSelect select) {
+ assertEquals("select has 2 children", 2, select.getChildElementCount());
+
+ for (DomElement child : select.getChildElements()) {
+ assertEquals("child element is an optgroup", "optgroup", child.getNodeName());
+ assertEquals("child has in turn 3 grandchildren", 3, child.getChildElementCount());
+
+ for (DomElement grandchild : child.getChildElements()) {
+ assertEquals("grandchild element is an option", "option", grandchild.getNodeName());
+ }
+ }
+
+ assertEquals("select element has 6 options", 6, select.getOptions().size());
+ HtmlOption option = select.getOptionByValue("5");
+ assertEquals("5th option is 'Audi'", "Audi", option.getText());
+ }
+}
diff --git a/test2/pom.xml b/test2/pom.xml
index 3e65aac785..dc23a72afe 100644
--- a/test2/pom.xml
+++ b/test2/pom.xml
@@ -29,6 +29,7 @@
faces22
faces23
+ faces40