diff --git a/java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java b/java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java
new file mode 100644
index 0000000000000..fcb51d62e0b28
--- /dev/null
+++ b/java/src/org/openqa/selenium/grid/data/AppiumRelaySlotMatcher.java
@@ -0,0 +1,103 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.openqa.selenium.grid.data;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.openqa.selenium.Capabilities;
+
+/**
+ * Opt-in matching implementation for Nodes that relay sessions to an Appium server. Unlike {@link
+ * DefaultSlotMatcher}, a stereotype that advertises Appium-awareness (an {@code appium:}-prefixed
+ * capability, or the non-W3C {@code platformVersion} signal) is treated as a wildcard for
+ * automationName, and a request carrying app-relay capabilities ({@link
+ * DefaultSlotMatcher#SPECIFIC_RELAY_CAPABILITIES_APP}) bypasses browserName/browserVersion
+ * matching. This lets a single relay slot serve varied automation frameworks and hybrid
+ * browser/native-app requests without the operator enumerating every client value in the
+ * stereotype.
+ *
+ *
Configure a Node to use this matcher instead of the default with:
+ *
+ *
+ * [distributor]
+ * slot-matcher = "org.openqa.selenium.grid.data.AppiumRelaySlotMatcher"
+ *
+ */
+public class AppiumRelaySlotMatcher implements SlotMatcher, Serializable {
+
+ private final DefaultSlotMatcher strict = new DefaultSlotMatcher();
+
+ @Override
+ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
+
+ if (capabilities.asMap().isEmpty()) {
+ return false;
+ }
+
+ if (!strict.initialMatch(stereotype, capabilities)) {
+ return false;
+ }
+
+ if (!strict.managedDownloadsEnabled(stereotype, capabilities)) {
+ return false;
+ }
+
+ if (!strict.extensionCapabilitiesMatch(stereotype, capabilities)) {
+ return false;
+ }
+
+ if (!automationNameMatch(stereotype, capabilities)) {
+ return false;
+ }
+
+ if (!strict.platformVersionMatch(stereotype, capabilities)) {
+ return false;
+ }
+
+ boolean browserNameMatch =
+ (capabilities.getBrowserName() == null || capabilities.getBrowserName().isEmpty())
+ || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName())
+ || DefaultSlotMatcher.matchConditionToRemoveCapability(capabilities);
+ boolean browserVersionMatch =
+ (capabilities.getBrowserVersion() == null
+ || capabilities.getBrowserVersion().isEmpty()
+ || Objects.equals(capabilities.getBrowserVersion(), "stable"))
+ || strict.browserVersionMatch(
+ stereotype.getBrowserVersion(), capabilities.getBrowserVersion())
+ || DefaultSlotMatcher.matchConditionToRemoveCapability(capabilities);
+ boolean platformNameMatch =
+ capabilities.getPlatformName() == null
+ || Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName())
+ || (stereotype.getPlatformName() != null
+ && stereotype.getPlatformName().is(capabilities.getPlatformName()));
+ return browserNameMatch && browserVersionMatch && platformNameMatch;
+ }
+
+ private boolean automationNameMatch(Capabilities stereotype, Capabilities capabilities) {
+ /*
+ A stereotype with no Appium-related capabilities at all has no relationship to a
+ requested automationName, so it should not match. Otherwise, an Appium-aware
+ stereotype is allowed to omit automationName and still match, since relay
+ stereotypes intentionally do this to serve varied automation sessions.
+ */
+ boolean stereotypeIsAppiumAware =
+ stereotype.getCapabilityNames().stream()
+ .anyMatch(name -> name.contains("platformVersion") || name.startsWith("appium:"));
+ return stereotypeIsAppiumAware || DefaultSlotMatcher.automationNameValue(capabilities) == null;
+ }
+}
diff --git a/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java b/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
index c7c822ca442fc..f2d1a561ab748 100644
--- a/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
+++ b/java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java
@@ -21,6 +21,7 @@
import java.io.Serializable;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import org.openqa.selenium.Capabilities;
@@ -56,6 +57,14 @@ public class DefaultSlotMatcher implements SlotMatcher, Serializable {
public static final List MANDATORY_CAPABILITIES =
List.of("platformName", "browserName", "browserVersion");
+ /**
+ * Determines whether {@code stereotype} is an acceptable match for a new session request carrying
+ * {@code capabilities}, per the class-level matching rules described above.
+ *
+ * @param stereotype the capabilities declared by a candidate {@link Slot}
+ * @param capabilities the capabilities requested for a new session
+ * @return {@code true} if the stereotype may serve the request
+ */
@Override
public boolean matches(Capabilities stereotype, Capabilities capabilities) {
@@ -75,6 +84,10 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
return false;
}
+ if (!automationNameMatch(stereotype, capabilities)) {
+ return false;
+ }
+
if (!platformVersionMatch(stereotype, capabilities)) {
return false;
}
@@ -82,14 +95,13 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
// At the end, a simple browser, browserVersion and platformName match
boolean browserNameMatch =
(capabilities.getBrowserName() == null || capabilities.getBrowserName().isEmpty())
- || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName())
- || matchConditionToRemoveCapability(capabilities);
+ || Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName());
boolean browserVersionMatch =
(capabilities.getBrowserVersion() == null
|| capabilities.getBrowserVersion().isEmpty()
|| Objects.equals(capabilities.getBrowserVersion(), "stable"))
- || browserVersionMatch(stereotype.getBrowserVersion(), capabilities.getBrowserVersion())
- || matchConditionToRemoveCapability(capabilities);
+ || browserVersionMatch(
+ stereotype.getBrowserVersion(), capabilities.getBrowserVersion());
boolean platformNameMatch =
capabilities.getPlatformName() == null
|| Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName())
@@ -98,11 +110,11 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
return browserNameMatch && browserVersionMatch && platformNameMatch;
}
- private boolean browserVersionMatch(String stereotype, String capabilities) {
+ boolean browserVersionMatch(String stereotype, String capabilities) {
return new SemanticVersionComparator().compare(stereotype, capabilities) == 0;
}
- private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) {
+ Boolean initialMatch(Capabilities stereotype, Capabilities capabilities) {
return stereotype.getCapabilityNames().stream()
// Matching of extension capabilities is implementation independent. Skip them
.filter(name -> !name.contains(":"))
@@ -128,7 +140,7 @@ private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities)
.orElse(true);
}
- private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) {
+ Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities capabilities) {
// First lets check if user wanted a Node with managed downloads enabled
Object raw = capabilities.getCapability(ENABLE_DOWNLOADS);
if (raw == null || !Boolean.parseBoolean(raw.toString())) {
@@ -141,7 +153,7 @@ private Boolean managedDownloadsEnabled(Capabilities stereotype, Capabilities ca
return raw != null && Boolean.parseBoolean(raw.toString());
}
- private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) {
+ Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) {
/*
This platform version match is not W3C compliant but users can add Appium servers as
Nodes, so we avoid delaying the match until the Slot, which makes the whole matching
@@ -158,7 +170,7 @@ private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capab
.orElse(true);
}
- private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities capabilities) {
+ Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities capabilities) {
/*
We match extension capabilities when they are not prefixed with any of the
EXTENSION_CAPABILITIES_PREFIXES items. Also, we match them only when the capabilities
@@ -188,6 +200,39 @@ private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities
.orElse(true);
}
+ Boolean automationNameMatch(Capabilities stereotype, Capabilities capabilities) {
+ /*
+ If the request specifies automationName (directly or nested in an options map), the
+ stereotype must declare the same value -- including the case where the stereotype
+ doesn't declare it at all. See https://github.com/SeleniumHQ/selenium/issues/17845.
+ */
+ Object requestedAutomationName = automationNameValue(capabilities);
+ if (requestedAutomationName == null) {
+ return true;
+ }
+ return Objects.equals(requestedAutomationName, automationNameValue(stereotype));
+ }
+
+ static Object automationNameValue(Capabilities capabilities) {
+ return capabilities.getCapabilityNames().stream()
+ .map(name -> automationNameValueFor(name, capabilities.getCapability(name)))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static Object automationNameValueFor(String name, Object value) {
+ if (name.equals("automationName") || name.endsWith(":automationName")) {
+ return value;
+ }
+ // automationName is sometimes nested inside an options map (e.g. appium:options) rather
+ // than sent as its own top-level capability.
+ if (name.toLowerCase().contains("options") && value instanceof Map) {
+ return ((Map, ?>) value).get("automationName");
+ }
+ return null;
+ }
+
public static Boolean matchConditionToRemoveCapability(Capabilities capabilities) {
/*
This match is specific for the Relay capabilities that are related to the Appium server for native application.
diff --git a/java/test/org/openqa/selenium/grid/data/AppiumRelaySlotMatcherTest.java b/java/test/org/openqa/selenium/grid/data/AppiumRelaySlotMatcherTest.java
new file mode 100644
index 0000000000000..b3e3734cd62dc
--- /dev/null
+++ b/java/test/org/openqa/selenium/grid/data/AppiumRelaySlotMatcherTest.java
@@ -0,0 +1,176 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.openqa.selenium.grid.data;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.ImmutableCapabilities;
+import org.openqa.selenium.Platform;
+import org.openqa.selenium.remote.CapabilityType;
+
+class AppiumRelaySlotMatcherTest {
+
+ private final AppiumRelaySlotMatcher slotMatcher = new AppiumRelaySlotMatcher();
+
+ @Test
+ void plainBrowserStereotypeStillRequiresExplicitParity() {
+ /*
+ Ordinary, non-relay matching is unaffected: ambient leniency only kicks in for
+ stereotypes that show Appium-awareness or requests carrying app-relay capabilities.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome", CapabilityType.PLATFORM_NAME, Platform.LINUX);
+ Capabilities matchingCapabilities =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "chrome", CapabilityType.PLATFORM_NAME, Platform.LINUX);
+ Capabilities mismatchedCapabilities =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME, "firefox", CapabilityType.PLATFORM_NAME, Platform.LINUX);
+
+ assertThat(slotMatcher.matches(stereotype, matchingCapabilities)).isTrue();
+ assertThat(slotMatcher.matches(stereotype, mismatchedCapabilities)).isFalse();
+ }
+
+ @Test
+ void automationNameDoesNotMatchWhenStereotypeIsNotAppiumAware() {
+ /*
+ Regression test for https://github.com/SeleniumHQ/selenium/issues/17845: a plain browser
+ stereotype with no Appium signal at all must still reject an automationName-differentiated
+ request, same as DefaultSlotMatcher.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "MicrosoftEdge",
+ CapabilityType.PLATFORM_NAME,
+ Platform.WIN10);
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ "appium:automationName", "Windows", CapabilityType.PLATFORM_NAME, Platform.WINDOWS);
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
+ }
+
+ @Test
+ void automationNameMatchesForAppiumAwareStereotypeMissingAutomationName() {
+ /*
+ An Appium-aware stereotype (declaring appium:platformVersion) may match a request carrying
+ automationName even though it doesn't declare that capability itself -- this is the
+ behavior relay-node operators opt into by choosing this matcher.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME,
+ Platform.ANDROID,
+ "appium:platformVersion",
+ "14",
+ "appium:automationName",
+ "uiautomator2");
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ }
+
+ @Test
+ void automationNameMatchesWhenNestedInsideOptionsMapForAppiumAwareStereotype() {
+ /*
+ An Appium-aware stereotype may still match a request nesting automationName inside an
+ options map (e.g. appium:options), consistent with top-level automationName handling.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME,
+ Platform.ANDROID,
+ "appium:platformVersion",
+ "14",
+ "appium:options",
+ Map.of("automationName", "uiautomator2"));
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ }
+
+ @Test
+ void relayNodeMatchesByBypassingBrowserNameWhenAppSet() {
+ /*
+ Relay node stereotype does not declare browserName (operator wants to restrict the slot to
+ native-app-only sessions). A request carrying both browserName (e.g. initialized via
+ ChromeOptions) and an app capability still matches -- the browserName is disregarded.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "chrome",
+ CapabilityType.PLATFORM_NAME,
+ Platform.ANDROID,
+ "appium:platformVersion",
+ "14",
+ "appium:app",
+ "link.to.apk",
+ "appium:automationName",
+ "uiautomator2");
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ }
+
+ @Test
+ void relayNodeRequiresBrowserNameWhenAppNotSet() {
+ /*
+ Relay node 1's stereotype does not declare browserName. A hybrid request (browserName set,
+ no app capability) must not match it -- the browserName bypass only applies when an
+ app-relay capability is present. Relay node 2's stereotype declares browserName and matches.
+ */
+ Capabilities stereotype1 =
+ new ImmutableCapabilities(
+ CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "chrome",
+ CapabilityType.PLATFORM_NAME,
+ Platform.ANDROID,
+ "appium:platformVersion",
+ "14",
+ "appium:automationName",
+ "uiautomator2");
+ assertThat(slotMatcher.matches(stereotype1, capabilities)).isFalse();
+
+ Capabilities stereotype2 =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "chrome",
+ CapabilityType.PLATFORM_NAME,
+ Platform.ANDROID,
+ "appium:platformVersion",
+ "14");
+ assertThat(slotMatcher.matches(stereotype2, capabilities)).isTrue();
+ }
+}
diff --git a/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java b/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
index 99ca4a4c722ef..81ea700fb2ffc 100644
--- a/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
+++ b/java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java
@@ -20,6 +20,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.openqa.selenium.remote.CapabilityType.ENABLE_DOWNLOADS;
+import java.util.Map;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.ImmutableCapabilities;
@@ -83,11 +84,12 @@ public void testSpecificRelayCapabilitiesAppMatch() {
}
@Test
- public void testRelayNodeMatchByRemovingBrowserNameWhenAppSet() {
+ public void testRelayNodeDoesNotBypassBrowserNameWhenAppSet() {
/*
- Relay node stereotype does not have browserName (where user wants to restrict to run a native app only)
- Request capabilities have both browserName (it might initialize by ChromeOptions) and app set
- The browserName will be filter out when validating match
+ DefaultSlotMatcher requires explicit parity: a stereotype that doesn't declare browserName
+ or automationName must not match a request carrying app-relay capabilities. The old
+ "filter out browserName when app is set" leniency now lives in AppiumRelaySlotMatcher,
+ which relay-node operators can opt into via the "slot-matcher" config.
*/
Capabilities stereotype =
new ImmutableCapabilities(
@@ -104,16 +106,16 @@ Request capabilities have both browserName (it might initialize by ChromeOptions
"link.to.apk",
"appium:automationName",
"uiautomator2");
- assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
}
@Test
- public void testRelayNodeNotMatchHybridBrowserVersionWhenStereotypeWithoutBrowserName() {
+ public void testRelayNodeRequiresExplicitAutomationNameParity() {
/*
- Relay node 1 has stereotype does not have browserName (where user wants to restrict to run a native app only)
- Request capabilities want to run a hybrid app (browserName is set) and app isn't set
- Request capabilities should not match the stereotype
- Relay node 2 has stereotype with browserName set should match the request capabilities
+ Neither relay stereotype declares automationName, so neither matches a hybrid request that
+ specifies it -- even the stereotype declaring browserName. Unlike the old gated behavior,
+ being Appium-aware (declaring appium:platformVersion) is not enough on its own; the
+ stereotype must declare the same automationName the request asks for.
*/
Capabilities stereotype1 =
new ImmutableCapabilities(
@@ -137,7 +139,7 @@ Request capabilities want to run a hybrid app (browserName is set) and app isn't
Platform.ANDROID,
"appium:platformVersion",
"14");
- assertThat(slotMatcher.matches(stereotype2, capabilities)).isTrue();
+ assertThat(slotMatcher.matches(stereotype2, capabilities)).isFalse();
}
@Test
@@ -162,9 +164,7 @@ public void testRelayNodeNotMatchWhenNonW3CCompliantPlatformVersionSet() {
CapabilityType.PLATFORM_NAME,
Platform.ANDROID,
"platformVersion",
- "15",
- "appium:automationName",
- "uiautomator2");
+ "15");
assertThat(slotMatcher.matches(stereotype1, capabilities)).isFalse();
Capabilities stereotype2 =
new ImmutableCapabilities(
@@ -752,4 +752,69 @@ void extensionCapsAlsoMatch() {
assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
}
+
+ @Test
+ void automationNameDoesNotMatchWhenStereotypeDeclaresNoExtensionCapabilities() {
+ /*
+ A plain browser stereotype must not match a request that only asks for automationName.
+ Regression test for https://github.com/SeleniumHQ/selenium/issues/17845
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "MicrosoftEdge",
+ CapabilityType.PLATFORM_NAME,
+ Platform.WIN10);
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ "appium:automationName", "Windows", CapabilityType.PLATFORM_NAME, Platform.WINDOWS);
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
+ }
+
+ @Test
+ void automationNameDoesNotMatchWhenStereotypeHasUnrelatedExtensionCapability() {
+ /*
+ A stereotype's unrelated, non-Appium extension capability must not be treated as a signal
+ that it can serve an automationName-differentiated request.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "MicrosoftEdge",
+ CapabilityType.PLATFORM_NAME,
+ Platform.WIN10,
+ "prefixed:cheese",
+ "amsterdam");
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ "appium:automationName", "Windows", CapabilityType.PLATFORM_NAME, Platform.WINDOWS);
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
+ }
+
+ @Test
+ void automationNameDoesNotMatchWhenNestedInsideOptionsMap() {
+ /*
+ A plain browser stereotype must not match a request that nests automationName inside an
+ options map (e.g. appium:options) instead of sending it as a top-level capability.
+ */
+ Capabilities stereotype =
+ new ImmutableCapabilities(
+ CapabilityType.BROWSER_NAME,
+ "MicrosoftEdge",
+ CapabilityType.PLATFORM_NAME,
+ Platform.WIN10);
+
+ Capabilities capabilities =
+ new ImmutableCapabilities(
+ "appium:options",
+ Map.of("automationName", "Windows"),
+ CapabilityType.PLATFORM_NAME,
+ Platform.WINDOWS);
+
+ assertThat(slotMatcher.matches(stereotype, capabilities)).isFalse();
+ }
}