Skip to content

Commit 3b98163

Browse files
Customizable test template display names (#591)
* customizable test template display names * check for meta-present TestTemplate annotations in SeleniumJupiter::supportsParameter to prevent ParameterResolutionException with competing TestTemplate ParameterResolver * simplify isTestTemplate Optional chaining
1 parent 62ea736 commit 3b98163

File tree

5 files changed

+286
-9
lines changed

5 files changed

+286
-9
lines changed

src/doc/asciidoc/index.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ TIP: You can use the capabilities annotations also for browsers in Docker (see h
211211
Selenium-Jupiter uses a standard feature of JUnit 5 called http://junit.org/junit5/docs/current/user-guide/#writing-tests-test-templates[test templates]. Test templates are a special kind of test in which the same test logic is executed several times according to some custom data. In Selenium-Jupiter, the data to feed a test template is known as _browser scenario_.
212212

213213
The following example shows a test using this feature.
214-
Notice that instead of declaring the method with the usual `@Test` annotation, we use the JUnit 5's `@TestTemplate`. Then, the parameter type of the test template method is `WebDriver`. This type is the generic interface of Selenium WebDriver, and the concise type (i.e. `ChromeDriver,` `FirefoxDriver,` etc.) is determined by Selenium-Jupiter in runtime.
214+
Notice that instead of declaring the method with the usual `@Test` annotation, we use the JUnit 5's `@TestTemplate`. Then, the parameter type of the test template method is `WebDriver`. This type is the generic interface of Selenium WebDriver, and the concise type (i.e. `ChromeDriver,` `FirefoxDriver,` etc.) is determined by Selenium-Jupiter in runtime. The `@BrowserScenarioTest` annotation can be used for more control over the test names generated by the template tests.
215215

216216
[source,java]
217217
----
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package io.github.bonigarcia.seljup;
2+
3+
import org.junit.jupiter.api.TestInfo;
4+
import org.junit.jupiter.api.TestTemplate;
5+
import org.junit.jupiter.api.extension.ExtensionContext;
6+
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
7+
import org.junit.platform.commons.util.Preconditions;
8+
9+
import java.lang.annotation.ElementType;
10+
import java.lang.annotation.Retention;
11+
import java.lang.annotation.RetentionPolicy;
12+
import java.lang.annotation.Target;
13+
import java.util.Arrays;
14+
import java.util.function.Function;
15+
import java.util.function.UnaryOperator;
16+
17+
/**
18+
* The annotated method is a <em>test template</em> that should be repeated for each
19+
* <a href="https://bonigarcia.dev/selenium-jupiter/#template-tests">browser scenario</a>
20+
* with a configurable {@linkplain #name display name}.
21+
*
22+
* @see org.junit.jupiter.api.TestTemplate
23+
*
24+
* @see org.junit.jupiter.api.RepeatedTest
25+
*/
26+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
27+
@Retention(RetentionPolicy.RUNTIME)
28+
@TestTemplate
29+
public @interface BrowserScenarioTest {
30+
31+
/**
32+
* Placeholder for the {@linkplain TestInfo#getDisplayName display name} of
33+
* a {@code @BrowserScenarioTest} method: <code>{displayName}</code>
34+
*/
35+
String DISPLAY_NAME_PLACEHOLDER = "{displayName}";
36+
37+
/**
38+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getType() browser type} of
39+
* a {@code @BrowserScenarioTest} method: <code>{type}</code>
40+
*/
41+
String TYPE_PLACEHOLDER = "{type}";
42+
43+
/**
44+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getVersion() browser version} of
45+
* a {@code @BrowserScenarioTest} method: <code>{version}</code>
46+
*/
47+
String VERSION_PLACEHOLDER = "{version}";
48+
49+
/**
50+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getArguments() browser arguments} of
51+
* a {@code @BrowserScenarioTest} method: <code>{arguments}</code>
52+
*/
53+
String ARGUMENTS_PLACEHOLDER = "{arguments}";
54+
55+
/**
56+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getPreferences() browser preferences} of
57+
* a {@code @BrowserScenarioTest} method: <code>{preferences}</code>
58+
*/
59+
String PREFERENCES_PLACEHOLDER = "{preferences}";
60+
61+
/**
62+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getCapabilities() capabilities} of
63+
* a {@code @BrowserScenarioTest} method: <code>{capabilities}</code>
64+
*/
65+
String CAPABILITIES_PLACEHOLDER = "{capabilities}";
66+
67+
/**
68+
* Placeholder for the {@linkplain BrowsersTemplate.Browser#getRemoteUrl() remoteUrl} of
69+
* a {@code @BrowserScenarioTest} method: <code>{remoteUrl}</code>
70+
*/
71+
String REMOTE_URL_PLACEHOLDER = "{remoteUrl}";
72+
73+
/**
74+
* default display name pattern for a browser scenario test: {@value}
75+
*/
76+
String DEFAULT_NAME = "{displayName} - {type} {version}";
77+
78+
/**
79+
* The display name for each browser scenario test.
80+
*
81+
* <h4>Supported placeholders</h4>
82+
* <ul>
83+
* <li>{@link #DISPLAY_NAME_PLACEHOLDER}</li>
84+
* <li>{@link #TYPE_PLACEHOLDER}</li>
85+
* <li>{@link #VERSION_PLACEHOLDER}</li>
86+
* <li>{@link #ARGUMENTS_PLACEHOLDER}</li>
87+
* <li>{@link #PREFERENCES_PLACEHOLDER}</li>
88+
* <li>{@link #CAPABILITIES_PLACEHOLDER}</li>
89+
* <li>{@link #REMOTE_URL_PLACEHOLDER}</li>
90+
* </ul>
91+
*
92+
* <p>Defaults to {@link #DEFAULT_NAME}, resulting in
93+
* names such as {@code "chat button - chrome latest [--window-size=1280,720]"}
94+
*
95+
* <p>Alternatively, you can provide a custom display name, optionally
96+
* using the aforementioned placeholders.
97+
*
98+
* @return a custom display name; never blank or consisting solely of
99+
* whitespace
100+
*/
101+
String name() default DEFAULT_NAME;
102+
103+
/**
104+
* A utility class for formatting the display name of a browser scenario test.
105+
*/
106+
final class NameFormatter {
107+
108+
/**
109+
* The constructor is private to prevent instantiation.
110+
*/
111+
private NameFormatter() {}
112+
113+
/**
114+
* Formats the display name of a browser scenario test.
115+
*
116+
* @param namePattern the name pattern from {@link BrowserScenarioTest#name()}
117+
* @param displayName the display name from {@link ExtensionContext#getDisplayName()}
118+
* @param browser the {@link BrowsersTemplate.Browser} from the {@link TestTemplateInvocationContext }
119+
* @return the formatted display name
120+
*/
121+
public static String format(String namePattern, String displayName, BrowsersTemplate.Browser browser) {
122+
Preconditions.notNull(namePattern, "namePattern must not be null");
123+
Preconditions.notNull(displayName, "displayName must not be null");
124+
Preconditions.notNull(browser, "browser must not be null");
125+
String result = namePattern.trim();
126+
Preconditions.notBlank(result, "@BrowserScenarioTest must be declared with a non-empty name.");
127+
result = replacePlaceholders(result, DISPLAY_NAME_PLACEHOLDER, displayName);
128+
result = replacePlaceholders(result, TYPE_PLACEHOLDER, browser.getType());
129+
result = replacePlaceholders(result, VERSION_PLACEHOLDER, browser.getVersion());
130+
result = replacePlaceholders(result, ARGUMENTS_PLACEHOLDER, browser.getArguments(), Arrays::toString);
131+
result = replacePlaceholders(result, PREFERENCES_PLACEHOLDER, browser.getPreferences(), Arrays::toString);
132+
result = replacePlaceholders(result, CAPABILITIES_PLACEHOLDER, browser.getCapabilities(), Object::toString);
133+
return replacePlaceholders(result, REMOTE_URL_PLACEHOLDER, browser.getRemoteUrl());
134+
}
135+
136+
/**
137+
* Null-safe placeholder replacement in the name pattern with the given string value. If the value is null,
138+
* replacement is skipped.
139+
*
140+
* @param namePattern the (not-nullable) pattern for string replacements
141+
* @param placeholder the (not-nullable) placeholder to be replaced
142+
* @param value the (nullable) string value to replace with which to replace the placeholder
143+
* @return the (non-null) string pattern with placeholders replaced by the given value
144+
*/
145+
private static String replacePlaceholders(String namePattern, String placeholder, String value) {
146+
return replacePlaceholders(namePattern, placeholder, value, UnaryOperator.identity());
147+
}
148+
149+
/**
150+
* Null-safe placeholder replacement in the name pattern with the given value transformed with a mapping
151+
* function. If the value is null, replacement is skipped.
152+
*
153+
* @param namePattern the (not-nullable) pattern for string replacements
154+
* @param placeholder the (not-nullable) placeholder to be replaced
155+
* @param value the (nullable) string value to replace with which to replace the placeholder
156+
* @param mapper a mapping function to transform the value into a suitable string
157+
* @return the (non-null) string pattern with placeholders replaced by the given value
158+
* @param <T> the generic type of the value
159+
*/
160+
private static <T> String replacePlaceholders(String namePattern, String placeholder, T value, Function<T, String> mapper) {
161+
if (value != null) {
162+
return namePattern.replace(placeholder, mapper.apply(value));
163+
} else {
164+
return namePattern;
165+
}
166+
}
167+
168+
}
169+
170+
}

src/main/java/io/github/bonigarcia/seljup/SeleniumJupiter.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.junit.jupiter.api.extension.ParameterResolver;
6161
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
6262
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
63+
import org.junit.platform.commons.support.AnnotationSupport;
6364
import org.openqa.selenium.Capabilities;
6465
import org.openqa.selenium.WebDriver;
6566
import org.openqa.selenium.devtools.DevTools;
@@ -528,7 +529,7 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
528529
// Registered browsers
529530
if (!browserListList.isEmpty()) {
530531
return browserListList.stream()
531-
.map(b -> invocationContext(b, this));
532+
.map(b -> invocationContext(b, this, extensionContext));
532533
}
533534

534535
// Browser scenario by content
@@ -556,13 +557,13 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
556557
if (!browserJsonContent.isEmpty()) {
557558
return new Gson()
558559
.fromJson(browserJsonContent, BrowsersTemplate.class)
559-
.getStream().map(b -> invocationContext(b, this));
560+
.getStream().map(b -> invocationContext(b, this, extensionContext));
560561
}
561562

562563
if (browserListMap != null) {
563564
List<Browser> browsers = browserListMap.get(contextId);
564565
if (browsers != null) {
565-
return Stream.of(invocationContext(browsers, this));
566+
return Stream.of(invocationContext(browsers, this, extensionContext));
566567
} else {
567568
return Stream.empty();
568569
}
@@ -577,11 +578,17 @@ public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContex
577578
}
578579

579580
private synchronized TestTemplateInvocationContext invocationContext(
580-
List<Browser> template, SeleniumJupiter parent) {
581+
List<Browser> template, SeleniumJupiter parent, ExtensionContext extensionContext) {
581582
return new TestTemplateInvocationContext() {
582583
@Override
583584
public String getDisplayName(int invocationIndex) {
584-
return template.toString();
585+
return AnnotationSupport.findAnnotation(
586+
extensionContext.getRequiredTestMethod(), BrowserScenarioTest.class)
587+
.map(annotation -> BrowserScenarioTest.NameFormatter.format(
588+
annotation.name(),
589+
extensionContext.getDisplayName(),
590+
template.get(0))
591+
).orElse(template.toString());
585592
}
586593

587594
@Override
@@ -652,9 +659,7 @@ public void putBrowserList(String key, List<Browser> browserList) {
652659
}
653660

654661
private boolean isTestTemplate(ExtensionContext extensionContext) {
655-
Optional<Method> testMethod = extensionContext.getTestMethod();
656-
return testMethod.isPresent()
657-
&& testMethod.get().isAnnotationPresent(TestTemplate.class);
662+
return AnnotationSupport.isAnnotated(extensionContext.getTestMethod(), TestTemplate.class);
658663
}
659664

660665
private boolean isGeneric(Class<?> type) {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.github.bonigarcia.seljup.test.template;
2+
3+
import com.google.gson.internal.LinkedTreeMap;
4+
import io.github.bonigarcia.seljup.BrowserBuilder;
5+
import io.github.bonigarcia.seljup.BrowserScenarioTest;
6+
import io.github.bonigarcia.seljup.BrowsersTemplate.Browser;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.platform.commons.PreconditionViolationException;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
13+
class BrowserScenarioFormattingTest {
14+
15+
@Test
16+
void replaceAllPlaceholders() {
17+
LinkedTreeMap<String, String> capabilities = new LinkedTreeMap<>();
18+
capabilities.put("custom:cap-1", "value-1");
19+
capabilities.put("custom:cap-2", "custom-value-2");
20+
capabilities.put("custom:cap-3", "true");
21+
Browser browser = new BrowserBuilder("chrome-in-docker")
22+
.version("latest")
23+
.arguments(new String[]{"--disable-gpu", "--no-sandbox"})
24+
.preferences(new String[]{"media.navigator.permission.disabled=true", "media.navigator.streams.fake=true"})
25+
.capabilities(capabilities)
26+
.remoteUrl("http://localhost:4444/")
27+
.build();
28+
String result = BrowserScenarioTest.NameFormatter.format(
29+
"{displayName}, {type}, {version}, {arguments}, {preferences}, {capabilities}, {remoteUrl}",
30+
"Sample Test",
31+
browser
32+
);
33+
assertEquals("""
34+
Sample Test, chrome-in-docker, latest, [--disable-gpu, --no-sandbox], \
35+
[media.navigator.permission.disabled=true, media.navigator.streams.fake=true], \
36+
{custom:cap-1=value-1, custom:cap-2=custom-value-2, custom:cap-3=true}, http://localhost:4444/""", result);
37+
}
38+
39+
@Test
40+
void skipNullBrowserAttributes() {
41+
Browser browser = new BrowserBuilder(null)
42+
.version(null)
43+
.arguments(null)
44+
.preferences(null)
45+
.capabilities(null)
46+
.remoteUrl(null)
47+
.build();
48+
String result = BrowserScenarioTest.NameFormatter.format(
49+
"{displayName}, {type}, {version}, {arguments}, {preferences}, {capabilities}, {remoteUrl}",
50+
"Sample Test",
51+
browser
52+
);
53+
assertEquals("Sample Test, {type}, {version}, {arguments}, {preferences}, {capabilities}, {remoteUrl}", result);
54+
}
55+
56+
@Test
57+
void throwExceptionWhenNamePatternIsNull() {
58+
Browser browser = new BrowserBuilder("chrome").build();
59+
assertThrows(PreconditionViolationException.class, () -> BrowserScenarioTest.NameFormatter.format(
60+
null,
61+
"Sample Test",
62+
browser
63+
));
64+
}
65+
66+
@Test
67+
void throwExceptionWhenDisplayNameIsNull() {
68+
Browser browser = new BrowserBuilder("chrome").build();
69+
assertThrows(PreconditionViolationException.class, () -> BrowserScenarioTest.NameFormatter.format(
70+
"{displayName} - {type} {version}",
71+
null,
72+
browser
73+
));
74+
}
75+
76+
@Test
77+
void throwExceptionWhenBrowserIsNull() {
78+
assertThrows(PreconditionViolationException.class, () -> BrowserScenarioTest.NameFormatter.format(
79+
"{displayName} - {type} {version}",
80+
"Sample Test",
81+
null
82+
));
83+
}
84+
85+
@Test
86+
void throwExceptionWhenNamePatternIsEmpty() {
87+
Browser browser = new BrowserBuilder("chrome").build();
88+
assertThrows(PreconditionViolationException.class, () -> BrowserScenarioTest.NameFormatter.format(
89+
" ",
90+
"Sample Test",
91+
browser
92+
));
93+
}
94+
95+
}

src/test/java/io/github/bonigarcia/seljup/test/template/TemplateTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
//tag::snippet-in-doc[]
2020
import static org.assertj.core.api.Assertions.assertThat;
2121

22+
import io.github.bonigarcia.seljup.BrowserScenarioTest;
2223
import org.junit.jupiter.api.TestTemplate;
2324
import org.junit.jupiter.api.extension.ExtendWith;
2425
import org.openqa.selenium.WebDriver;
@@ -34,5 +35,11 @@ void templateTest(WebDriver driver) {
3435
assertThat(driver.getTitle()).contains("Selenium WebDriver");
3536
}
3637

38+
@BrowserScenarioTest(name = "Templated Test Name - {type} {version}")
39+
void namePatternTest(WebDriver driver) {
40+
driver.get("https://bonigarcia.dev/selenium-webdriver-java/");
41+
assertThat(driver.getTitle()).contains("Selenium WebDriver");
42+
}
43+
3744
}
3845
//end::snippet-in-doc[]

0 commit comments

Comments
 (0)