diff --git a/jetty-websocket/websocket-core-common/src/main/java/module-info.java b/jetty-websocket/websocket-core-common/src/main/java/module-info.java
index 9afd3e526aa0..822e3c6c6ac1 100644
--- a/jetty-websocket/websocket-core-common/src/main/java/module-info.java
+++ b/jetty-websocket/websocket-core-common/src/main/java/module-info.java
@@ -24,7 +24,8 @@
exports org.eclipse.jetty.websocket.core.internal to
org.eclipse.jetty.websocket.core.client,
- org.eclipse.jetty.websocket.core.server;
+ org.eclipse.jetty.websocket.core.server,
+ org.eclipse.jetty.util; // Export to DecoratedObjectFactory.
// The Jetty & Javax API Layers need to access both access some internal utilities which we don't want to expose.
exports org.eclipse.jetty.websocket.core.internal.util to
diff --git a/tests/pom.xml b/tests/pom.xml
index 36993d7cc35b..a8140e9c0bdf 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -56,5 +56,6 @@
test-http-client-transport
test-distribution
test-cdi
+ test-jpms
diff --git a/tests/test-jpms/pom.xml b/tests/test-jpms/pom.xml
new file mode 100644
index 000000000000..0acfe67a511e
--- /dev/null
+++ b/tests/test-jpms/pom.xml
@@ -0,0 +1,33 @@
+
+
+
+ org.eclipse.jetty.tests
+ tests-parent
+ 10.0.7-SNAPSHOT
+
+ 4.0.0
+ test-jpms
+ pom
+ Jetty Tests :: JPMS Parent
+
+ test-jpms-websocket-core
+
+
+
+
+ org.eclipse.jetty
+ jetty-slf4j-impl
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.eclipse.jetty.toolchain
+ jetty-test-helper
+ test
+
+
+
+
diff --git a/tests/test-jpms/test-jpms-websocket-core/pom.xml b/tests/test-jpms/test-jpms-websocket-core/pom.xml
new file mode 100644
index 000000000000..93f862c4c1ed
--- /dev/null
+++ b/tests/test-jpms/test-jpms-websocket-core/pom.xml
@@ -0,0 +1,25 @@
+
+
+
+ test-jpms
+ org.eclipse.jetty.tests
+ 10.0.7-SNAPSHOT
+
+ 4.0.0
+ test-jpms-websocket-core
+ jar
+ Jetty Tests :: JPMS :: WebSocket Core Tests
+
+
+
+ org.eclipse.jetty.websocket
+ websocket-core-server
+ ${project.version}
+
+
+ org.eclipse.jetty.websocket
+ websocket-core-client
+ ${project.version}
+
+
+
diff --git a/tests/test-jpms/test-jpms-websocket-core/src/main/java/module-info.java b/tests/test-jpms/test-jpms-websocket-core/src/main/java/module-info.java
new file mode 100644
index 000000000000..c10573d1531f
--- /dev/null
+++ b/tests/test-jpms/test-jpms-websocket-core/src/main/java/module-info.java
@@ -0,0 +1,22 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+//
+// 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.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+/**
+ * This module-info.java exists so that the tests can be run in JPMS mode,
+ * therefore testing the JPMS module descriptors of the dependencies involved.
+ */
+module org.eclipse.jetty.websocket.core.tests
+{
+ requires org.eclipse.jetty.websocket.core.server;
+ requires org.eclipse.jetty.websocket.core.client;
+}
diff --git a/tests/test-jpms/test-jpms-websocket-core/src/test/java/WebSocketCoreJPMSTest.java b/tests/test-jpms/test-jpms-websocket-core/src/test/java/WebSocketCoreJPMSTest.java
new file mode 100644
index 000000000000..f5533189a2d7
--- /dev/null
+++ b/tests/test-jpms/test-jpms-websocket-core/src/test/java/WebSocketCoreJPMSTest.java
@@ -0,0 +1,115 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
+//
+// 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.
+//
+// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+// ========================================================================
+//
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.websocket.core.CloseStatus;
+import org.eclipse.jetty.websocket.core.CoreSession;
+import org.eclipse.jetty.websocket.core.Frame;
+import org.eclipse.jetty.websocket.core.FrameHandler;
+import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest;
+import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
+import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator;
+import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WebSocketCoreJPMSTest
+{
+ private Server _server;
+ private ServerConnector _serverConnector;
+ private WebSocketCoreClient _client;
+
+ @BeforeEach
+ public void before() throws Exception
+ {
+ _server = new Server();
+ _serverConnector = new ServerConnector(_server);
+ _server.addConnector(_serverConnector);
+
+ WebSocketUpgradeHandler webSocketUpgradeHandler = new WebSocketUpgradeHandler();
+ FrameHandler myFrameHandler = new TestFrameHandler("Server");
+ webSocketUpgradeHandler.addMapping("/ws", WebSocketNegotiator.from(negotiation -> myFrameHandler));
+
+ _server.setHandler(webSocketUpgradeHandler);
+ _server.start();
+
+ _client = new WebSocketCoreClient();
+ _client.start();
+ }
+
+ @AfterEach
+ public void after() throws Exception
+ {
+ _client.stop();
+ _server.stop();
+ }
+
+ @Test
+ public void testSimpleEcho() throws Exception
+ {
+ TestFrameHandler frameHandler = new TestFrameHandler("Client");
+ URI uri = URI.create("ws://localhost:" + _serverConnector.getLocalPort() + "/ws");
+ CoreClientUpgradeRequest upgradeRequest = CoreClientUpgradeRequest.from(_client, uri, frameHandler);
+ upgradeRequest.addExtensions("permessage-deflate");
+ CoreSession coreSession = _client.connect(upgradeRequest).get(5, TimeUnit.SECONDS);
+ coreSession.close(Callback.NOOP);
+ }
+
+ public static class TestFrameHandler implements FrameHandler
+ {
+ private static final Logger LOG = LoggerFactory.getLogger(TestFrameHandler.class);
+
+ private final String _id;
+
+ public TestFrameHandler(String id)
+ {
+ _id = id;
+ }
+
+ @Override
+ public void onOpen(CoreSession coreSession, Callback callback)
+ {
+ LOG.info(_id + " onOpen");
+ callback.succeeded();
+ }
+
+ @Override
+ public void onFrame(Frame frame, Callback callback)
+ {
+ LOG.info(_id + " onFrame");
+ callback.succeeded();
+ }
+
+ @Override
+ public void onError(Throwable cause, Callback callback)
+ {
+ LOG.info(_id + " onError");
+ callback.succeeded();
+ }
+
+ @Override
+ public void onClosed(CloseStatus closeStatus, Callback callback)
+ {
+ LOG.info(_id + " onClosed");
+ callback.succeeded();
+ }
+ }
+}