Skip to content

Commit 4cacd4a

Browse files
authored
[grid] Remove browserName capability from stereotype and SlotMatcher when using Relay Node to test a mobile application (#15537)
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
1 parent bce221b commit 4cacd4a

File tree

5 files changed

+242
-17
lines changed

5 files changed

+242
-17
lines changed

java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java

+30-7
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public class DefaultSlotMatcher implements SlotMatcher, Serializable {
5050
*/
5151
private static final List<String> EXTENSION_CAPABILITIES_PREFIXES =
5252
Arrays.asList("goog:", "moz:", "ms:", "safari:", "se:");
53+
public static final List<String> SPECIFIC_RELAY_CAPABILITIES_APP =
54+
Arrays.asList("appium:app", "appium:appPackage", "appium:bundleId");
55+
public static final List<String> MANDATORY_CAPABILITIES =
56+
Arrays.asList("platformName", "browserName", "browserVersion");
5357

5458
@Override
5559
public boolean matches(Capabilities stereotype, Capabilities capabilities) {
@@ -66,24 +70,25 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
6670
return false;
6771
}
6872

69-
if (!platformVersionMatch(stereotype, capabilities)) {
73+
if (!extensionCapabilitiesMatch(stereotype, capabilities)) {
7074
return false;
7175
}
7276

73-
if (!extensionCapabilitiesMatch(stereotype, capabilities)) {
77+
if (!platformVersionMatch(stereotype, capabilities)) {
7478
return false;
7579
}
7680

7781
// At the end, a simple browser, browserVersion and platformName match
7882
boolean browserNameMatch =
7983
(capabilities.getBrowserName() == null || capabilities.getBrowserName().isEmpty())
80-
|| Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName());
84+
|| Objects.equals(stereotype.getBrowserName(), capabilities.getBrowserName())
85+
|| matchConditionToRemoveCapability(capabilities);
8186
boolean browserVersionMatch =
8287
(capabilities.getBrowserVersion() == null
8388
|| capabilities.getBrowserVersion().isEmpty()
8489
|| Objects.equals(capabilities.getBrowserVersion(), "stable"))
85-
|| browserVersionMatch(
86-
stereotype.getBrowserVersion(), capabilities.getBrowserVersion());
90+
|| browserVersionMatch(stereotype.getBrowserVersion(), capabilities.getBrowserVersion())
91+
|| matchConditionToRemoveCapability(capabilities);
8792
boolean platformNameMatch =
8893
capabilities.getPlatformName() == null
8994
|| Objects.equals(stereotype.getPlatformName(), capabilities.getPlatformName())
@@ -100,8 +105,11 @@ private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities)
100105
return stereotype.getCapabilityNames().stream()
101106
// Matching of extension capabilities is implementation independent. Skip them
102107
.filter(name -> !name.contains(":"))
103-
// Platform matching is special, we do it later
104-
.filter(name -> !"platformName".equalsIgnoreCase(name))
108+
// Mandatory capabilities match is done at the end
109+
.filter(
110+
name ->
111+
MANDATORY_CAPABILITIES.stream()
112+
.noneMatch(mandatory -> mandatory.equalsIgnoreCase(name)))
105113
.map(
106114
name -> {
107115
if (capabilities.getCapability(name) instanceof String) {
@@ -178,4 +186,19 @@ private Boolean extensionCapabilitiesMatch(Capabilities stereotype, Capabilities
178186
.reduce(Boolean::logicalAnd)
179187
.orElse(true);
180188
}
189+
190+
public static Boolean matchConditionToRemoveCapability(Capabilities capabilities) {
191+
/*
192+
This match is specific for the Relay capabilities that are related to the Appium server for native application.
193+
- If browserName is defined then we always assume it’s a hybrid browser session, so no app-related caps should be provided
194+
- If app is provided then the assumption is that app should be fetched from somewhere first and then installed on the destination device
195+
- If only appPackage is provided for uiautomator2 driver or bundleId for xcuitest then the assumption is we want to automate an app that is already installed on the device under test
196+
- If both (app and appPackage) or (app and bundleId). This will then save some small-time for the driver as by default it tries to autodetect these values anyway by analyzing the fetched package’s manifest
197+
*/
198+
return SPECIFIC_RELAY_CAPABILITIES_APP.stream()
199+
.anyMatch(
200+
name ->
201+
capabilities.getCapability(name) != null
202+
&& !capabilities.getCapability(name).toString().isEmpty());
203+
}
181204
}

java/src/org/openqa/selenium/grid/node/relay/RelaySessionFactory.java

+16-10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package org.openqa.selenium.grid.node.relay;
1919

20+
import static org.openqa.selenium.grid.data.DefaultSlotMatcher.matchConditionToRemoveCapability;
2021
import static org.openqa.selenium.remote.RemoteTags.CAPABILITIES;
2122
import static org.openqa.selenium.remote.RemoteTags.CAPABILITIES_EVENT;
2223
import static org.openqa.selenium.remote.tracing.AttributeKey.DOWNSTREAM_DIALECT;
@@ -72,7 +73,6 @@
7273
public class RelaySessionFactory implements SessionFactory {
7374

7475
private static final Logger LOG = Logger.getLogger(RelaySessionFactory.class.getName());
75-
7676
private final Tracer tracer;
7777
private final HttpClient.Factory clientFactory;
7878
private final Duration sessionTimeout;
@@ -140,24 +140,30 @@ public boolean test(Capabilities capabilities) {
140140
.orElse(false);
141141
}
142142

143+
public Capabilities filterRelayCapabilities(Capabilities capabilities) {
144+
/*
145+
Remove browserName capability if 'appium:app' (or similar based on driver) is present as it breaks appium tests when app is provided
146+
they are mutually exclusive
147+
*/
148+
if (matchConditionToRemoveCapability(capabilities)) {
149+
MutableCapabilities filteredStereotype = new MutableCapabilities(capabilities);
150+
filteredStereotype.setCapability(CapabilityType.BROWSER_NAME, (String) null);
151+
return filteredStereotype;
152+
}
153+
return capabilities;
154+
}
155+
143156
@Override
144157
public Either<WebDriverException, ActiveSession> apply(CreateSessionRequest sessionRequest) {
145158
Capabilities capabilities = sessionRequest.getDesiredCapabilities();
159+
capabilities = filterRelayCapabilities(capabilities);
160+
146161
if (!test(capabilities)) {
147162
return Either.left(
148163
new SessionNotCreatedException(
149164
"New session request capabilities do not " + "match the stereotype."));
150165
}
151166

152-
// remove browserName capability if 'appium:app' is present as it breaks appium tests when app
153-
// is provided
154-
// they are mutually exclusive
155-
MutableCapabilities filteredStereotype = new MutableCapabilities(stereotype);
156-
if (capabilities.getCapability("appium:app") != null) {
157-
filteredStereotype.setCapability(CapabilityType.BROWSER_NAME, (String) null);
158-
}
159-
160-
capabilities = capabilities.merge(filteredStereotype);
161167
LOG.info("Starting session for " + capabilities);
162168

163169
try (Span span = tracer.getCurrentContext().createSpan("relay_session_factory.apply")) {

java/test/org/openqa/selenium/grid/data/DefaultSlotMatcherTest.java

+123
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,129 @@ void testBrowserVersionMatch() {
5252
assertThat(comparator.compare("130.0", "131")).isEqualTo(-1);
5353
}
5454

55+
@Test
56+
public void testSpecificRelayCapabilitiesAppMatch() {
57+
Capabilities capabilitiesWithApp =
58+
new ImmutableCapabilities(
59+
"appium:app",
60+
"link.to.apk",
61+
"appium:appPackage",
62+
"com.example.app",
63+
"appium:platformVersion",
64+
"15",
65+
"platformName",
66+
"Android",
67+
"appium:automationName",
68+
"uiautomator2");
69+
assertThat(DefaultSlotMatcher.matchConditionToRemoveCapability(capabilitiesWithApp)).isTrue();
70+
capabilitiesWithApp =
71+
new ImmutableCapabilities(
72+
"browserName",
73+
"chrome",
74+
"appium:platformVersion",
75+
"15",
76+
"platformName",
77+
"Android",
78+
"appium:automationName",
79+
"uiautomator2");
80+
assertThat(DefaultSlotMatcher.matchConditionToRemoveCapability(capabilitiesWithApp)).isFalse();
81+
}
82+
83+
@Test
84+
public void testRelayNodeMatchByRemovingBrowserNameWhenAppSet() {
85+
/*
86+
Relay node stereotype does not have browserName (where user wants to restrict to run a native app only)
87+
Request capabilities have both browserName (it might initialize by ChromeOptions) and app set
88+
The browserName will be filter out when validating match
89+
*/
90+
Capabilities stereotype =
91+
new ImmutableCapabilities(
92+
CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
93+
Capabilities capabilities =
94+
new ImmutableCapabilities(
95+
CapabilityType.BROWSER_NAME,
96+
"chrome",
97+
CapabilityType.PLATFORM_NAME,
98+
Platform.ANDROID,
99+
"appium:platformVersion",
100+
"14",
101+
"appium:app",
102+
"link.to.apk",
103+
"appium:automationName",
104+
"uiautomator2");
105+
assertThat(slotMatcher.matches(stereotype, capabilities)).isTrue();
106+
}
107+
108+
@Test
109+
public void testRelayNodeNotMatchHybridBrowserVersionWhenStereotypeWithoutBrowserName() {
110+
/*
111+
Relay node 1 has stereotype does not have browserName (where user wants to restrict to run a native app only)
112+
Request capabilities want to run a hybrid app (browserName is set) and app isn't set
113+
Request capabilities should not match the stereotype
114+
Relay node 2 has stereotype with browserName set should match the request capabilities
115+
*/
116+
Capabilities stereotype1 =
117+
new ImmutableCapabilities(
118+
CapabilityType.PLATFORM_NAME, Platform.ANDROID, "appium:platformVersion", "14");
119+
Capabilities capabilities =
120+
new ImmutableCapabilities(
121+
CapabilityType.BROWSER_NAME,
122+
"chrome",
123+
CapabilityType.PLATFORM_NAME,
124+
Platform.ANDROID,
125+
"appium:platformVersion",
126+
"14",
127+
"appium:automationName",
128+
"uiautomator2");
129+
assertThat(slotMatcher.matches(stereotype1, capabilities)).isFalse();
130+
Capabilities stereotype2 =
131+
new ImmutableCapabilities(
132+
CapabilityType.BROWSER_NAME,
133+
"chrome",
134+
CapabilityType.PLATFORM_NAME,
135+
Platform.ANDROID,
136+
"appium:platformVersion",
137+
"14");
138+
assertThat(slotMatcher.matches(stereotype2, capabilities)).isTrue();
139+
}
140+
141+
@Test
142+
public void testRelayNodeNotMatchWhenNonW3CCompliantPlatformVersionSet() {
143+
/*
144+
There are Appium server plugins which allow to “fix” non W3C compliant capabilities by automatically adding `appium:` prefix to them
145+
Relay Node 1: When `platformVersion` is set in both stereotype and capabilities, the non W3C compliant `platformVersion` should be not matched
146+
Relay Node 2: When `platformVersion` is set in stereotype and capabilities, the non W3C compliant `platformVersion` should be matched
147+
*/
148+
Capabilities stereotype1 =
149+
new ImmutableCapabilities(
150+
CapabilityType.BROWSER_NAME,
151+
"chrome",
152+
CapabilityType.PLATFORM_NAME,
153+
Platform.ANDROID,
154+
"platformVersion",
155+
"14");
156+
Capabilities capabilities =
157+
new ImmutableCapabilities(
158+
CapabilityType.BROWSER_NAME,
159+
"chrome",
160+
CapabilityType.PLATFORM_NAME,
161+
Platform.ANDROID,
162+
"platformVersion",
163+
"15",
164+
"appium:automationName",
165+
"uiautomator2");
166+
assertThat(slotMatcher.matches(stereotype1, capabilities)).isFalse();
167+
Capabilities stereotype2 =
168+
new ImmutableCapabilities(
169+
CapabilityType.BROWSER_NAME,
170+
"chrome",
171+
CapabilityType.PLATFORM_NAME,
172+
Platform.ANDROID,
173+
"platformVersion",
174+
"15");
175+
assertThat(slotMatcher.matches(stereotype2, capabilities)).isTrue();
176+
}
177+
55178
@Test
56179
void fullMatch() {
57180
Capabilities stereotype =

java/test/org/openqa/selenium/grid/node/relay/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ java_test_suite(
1515
"//java/test/org/openqa/selenium/remote/tracing:tracing-support",
1616
artifact("org.junit.jupiter:junit-jupiter-api"),
1717
artifact("org.assertj:assertj-core"),
18+
artifact("org.mockito:mockito-core"),
1819
] + JUNIT5_DEPS,
1920
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.node.relay;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.mockito.Mockito.when;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.mockito.Mockito;
25+
import org.openqa.selenium.Capabilities;
26+
import org.openqa.selenium.ImmutableCapabilities;
27+
28+
public class RelaySessionFactoryTest {
29+
30+
// Add the following test method to the `RelaySessionFactoryTest` class
31+
@Test
32+
public void testFilterRelayCapabilities() {
33+
Capabilities capabilitiesWithApp =
34+
new ImmutableCapabilities(
35+
"browserName", "chrome", "platformName", "Android", "appium:app", "/link/to/app.apk");
36+
Capabilities capabilitiesWithAppPackage =
37+
new ImmutableCapabilities(
38+
"browserName",
39+
"chrome",
40+
"platformName",
41+
"Android",
42+
"appium:appPackage",
43+
"com.example.app");
44+
Capabilities capabilitiesWithBundleId =
45+
new ImmutableCapabilities(
46+
"browserName",
47+
"chrome",
48+
"platformName",
49+
"Android",
50+
"appium:bundleId",
51+
"com.example.app");
52+
Capabilities capabilitiesWithoutApp =
53+
new ImmutableCapabilities("browserName", "chrome", "platformName", "Android");
54+
55+
RelaySessionFactory factory = Mockito.mock(RelaySessionFactory.class);
56+
57+
when(factory.filterRelayCapabilities(capabilitiesWithApp)).thenCallRealMethod();
58+
when(factory.filterRelayCapabilities(capabilitiesWithAppPackage)).thenCallRealMethod();
59+
when(factory.filterRelayCapabilities(capabilitiesWithBundleId)).thenCallRealMethod();
60+
when(factory.filterRelayCapabilities(capabilitiesWithoutApp)).thenCallRealMethod();
61+
62+
capabilitiesWithApp = factory.filterRelayCapabilities(capabilitiesWithApp);
63+
capabilitiesWithAppPackage = factory.filterRelayCapabilities(capabilitiesWithAppPackage);
64+
capabilitiesWithBundleId = factory.filterRelayCapabilities(capabilitiesWithBundleId);
65+
capabilitiesWithoutApp = factory.filterRelayCapabilities(capabilitiesWithoutApp);
66+
67+
assertEquals(null, capabilitiesWithApp.getCapability("browserName"));
68+
assertEquals(null, capabilitiesWithAppPackage.getCapability("browserName"));
69+
assertEquals(null, capabilitiesWithBundleId.getCapability("browserName"));
70+
assertEquals("chrome", capabilitiesWithoutApp.getCapability("browserName"));
71+
}
72+
}

0 commit comments

Comments
 (0)