未验证 提交 53e59bb0 编写于 作者: V Vincent Latombe 提交者: GitHub

Expose `org.jenkins.ui.symbol.Symbol` (#6659)

* Expose org.jenkins.ui.symbol.Symbol

* Exposes the existing IconSet#getSymbol for plugins by adding a builder
pattern and moving it to its own package and class to separate the
symbol framework from the Icon one.
* Added coverage to the existing method including symbol lookup from
  plugins

* Fixes JENKINS-68805 and pick up #6685

* Clean up

* Rename method since the intent slightly changed

* Simplify

* Make this compile

* Remove support for legacy names

* Fix reviews

* Add assertions for the symbol content used for testing

* Read science svg from filesystem

* Add withRaw to parse a raw selector
Co-authored-by: NDaniel Beck <daniel-beck@users.noreply.github.com>
Co-authored-by: NBasil Crow <me@basilcrow.com>
上级 c6afa1a7
......@@ -26,22 +26,15 @@ package org.jenkins.ui.icon;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Functions;
import hudson.PluginWrapper;
import hudson.Util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.symbol.Symbol;
import org.jenkins.ui.symbol.SymbolRequest;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
......@@ -54,15 +47,12 @@ public class IconSet {
public static final IconSet icons = new IconSet();
// keyed by plugin name / core, and then symbol name returning the SVG as a string
private static final Map<String, Map<String, String>> SYMBOLS = new ConcurrentHashMap<>();
private Map<String, Icon> iconsByCSSSelector = new ConcurrentHashMap<>();
private Map<String, Icon> iconsByUrl = new ConcurrentHashMap<>();
private Map<String, Icon> iconsByClassSpec = new ConcurrentHashMap<>();
private Map<String, Icon> coreIcons = new ConcurrentHashMap<>();
private static final String PLACEHOLDER_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"ionicon\" height=\"48\" viewBox=\"0 0 512 512\"><title>Close</title><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"32\" d=\"M368 368L144 144M368 144L144 368\"/></svg>";
private static final Icon NO_ICON = new Icon("_", "_", "_");
public IconSet() {
......@@ -76,90 +66,20 @@ public class IconSet {
context.setVariable("icons", icons);
}
private static String prependTitleIfRequired(String icon, String title) {
if (StringUtils.isNotBlank(title)) {
return "<span class=\"jenkins-visually-hidden\">" + Util.xmlEscape(title) + "</span>" + icon;
}
return icon;
}
// for Jelly
@SuppressWarnings("unused")
@Restricted(NoExternalUse.class)
public static String getSymbol(String name, String title, String tooltip, String htmlTooltip, String classes, String pluginName, String id) {
String translatedName = cleanName(name);
String identifier = Util.fixEmpty(pluginName) == null ? "core" : pluginName;
Map<String, String> symbolsForLookup = SYMBOLS.computeIfAbsent(identifier, (key) -> new ConcurrentHashMap<>());
if (symbolsForLookup.containsKey(translatedName)) {
String symbol = symbolsForLookup.get(translatedName);
symbol = symbol.replaceAll("(class=\").*?(\")", "$1$2");
symbol = symbol.replaceAll("(tooltip=\").*?(\")", "");
symbol = symbol.replaceAll("(data-html-tooltip=\").*?(\")", "");
symbol = symbol.replaceAll("(id=\").*?(\")", "");
if (!tooltip.isEmpty() && htmlTooltip.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg tooltip=\"" + Functions.htmlAttributeEscape(tooltip) + "\"");
}
if (!htmlTooltip.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg data-html-tooltip=\"" + Functions.htmlAttributeEscape(htmlTooltip) + "\"");
}
if (!id.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg id=\"" + Functions.htmlAttributeEscape(id) + "\"");
}
symbol = symbol.replaceAll("<svg", "<svg class=\"" + Functions.htmlAttributeEscape(classes) + "\"");
return prependTitleIfRequired(symbol, title);
}
// Load symbol if it exists
InputStream inputStream = getClassLoader(identifier).getResourceAsStream("images/symbols/" + translatedName + ".svg");
String symbol = null;
try {
if (inputStream != null) {
symbol = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
}
} catch (IOException e) {
// ignored
}
if (symbol == null) {
symbol = PLACEHOLDER_SVG;
}
symbol = symbol.replaceAll("(<title>).*(</title>)", "$1$2");
symbol = symbol.replaceAll("(class=\").*?(\")", "$1$2");
symbol = symbol.replaceAll("(tooltip=\").*?(\")", "$1$2");
symbol = symbol.replaceAll("(data-html-tooltip=\").*?(\")", "$1$2");
symbol = symbol.replaceAll("(id=\").*?(\")", "");
if (!tooltip.isEmpty() && htmlTooltip.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg tooltip=\"" + Functions.htmlAttributeEscape(tooltip) + "\"");
}
if (!htmlTooltip.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg data-html-tooltip=\"" + Functions.htmlAttributeEscape(htmlTooltip) + "\"");
}
if (!id.isEmpty()) {
symbol = symbol.replaceAll("<svg", "<svg id=\"" + Functions.htmlAttributeEscape(id) + "\"");
}
symbol = symbol.replaceAll("<svg", "<svg aria-hidden=\"true\"");
symbol = symbol.replaceAll("<svg", "<svg class=\"" + Functions.htmlAttributeEscape(classes) + "\"");
symbol = symbol.replace("stroke:#000", "stroke:currentColor");
symbolsForLookup.put(translatedName, symbol);
SYMBOLS.put(identifier, symbolsForLookup);
return prependTitleIfRequired(symbol, title);
}
private static ClassLoader getClassLoader(String pluginName) {
if (pluginName.equals("core")) {
return IconSet.class.getClassLoader();
}
PluginWrapper plugin = Jenkins.get().getPluginManager().getPlugin(pluginName);
if (plugin != null) {
return plugin.classLoader;
}
return IconSet.class.getClassLoader();
return Symbol.get(new SymbolRequest.Builder()
.withName(IconSet.cleanName(name))
.withTitle(title)
.withTooltip(tooltip)
.withHtmlTooltip(htmlTooltip)
.withClasses(classes)
.withPluginName(pluginName)
.withId(id)
.build()
);
}
public IconSet addIcon(Icon icon) {
......
package org.jenkins.ui.symbol;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Functions;
import hudson.PluginWrapper;
import hudson.Util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
/**
* Helper class to load symbols from Jenkins core or plugins.
* @since TODO
*/
public final class Symbol {
private static final Logger LOGGER = Logger.getLogger(Symbol.class.getName());
// keyed by plugin name / core, and then symbol name returning the SVG as a string
private static final Map<String, Map<String, String>> SYMBOLS = new ConcurrentHashMap<>();
static final String PLACEHOLDER_SVG =
"<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"ionicon\" height=\"48\" viewBox=\"0 0 512 512\"><title>Close</title><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"32\" d=\"M368 368L144 144M368 144L144 368\"/></svg>";
/**
* A substring of the placeholder so that tests can match it.
*/
static final String PLACEHOLDER_MATCHER = "M368 368L144 144M368 144L144 368";
private Symbol() {}
/**
* Generates the svg markup for the given symbol name and attributes.
* @param request the symbol request object.
* @return The svg markup for the symbol.
* @since TODO
*/
public static String get(@NonNull SymbolRequest request) {
String name = request.getName();
String title = request.getTitle();
String tooltip = request.getTooltip();
String htmlTooltip = request.getHtmlTooltip();
String classes = request.getClasses();
String pluginName = request.getPluginName();
String id = request.getId();
String identifier = StringUtils.defaultIfBlank(pluginName, "core");
String symbol = SYMBOLS
.computeIfAbsent(identifier, key -> new ConcurrentHashMap<>())
.computeIfAbsent(name, key -> loadSymbol(identifier, key));
if (StringUtils.isNotBlank(tooltip) && StringUtils.isBlank(htmlTooltip)) {
symbol = symbol.replaceAll("<svg", "<svg tooltip=\"" + Functions.htmlAttributeEscape(tooltip) + "\"");
}
if (StringUtils.isNotBlank(htmlTooltip)) {
symbol = symbol.replaceAll("<svg", "<svg data-html-tooltip=\"" + Functions.htmlAttributeEscape(htmlTooltip) + "\"");
}
if (StringUtils.isNotBlank(id)) {
symbol = symbol.replaceAll("<svg", "<svg id=\"" + Functions.htmlAttributeEscape(id) + "\"");
}
if (StringUtils.isNotBlank(classes)) {
symbol = symbol.replaceAll("<svg", "<svg class=\"" + Functions.htmlAttributeEscape(classes) + "\"");
}
if (StringUtils.isNotBlank(title)) {
symbol = "<span class=\"jenkins-visually-hidden\">" + Util.xmlEscape(title) + "</span>" + symbol;
}
return symbol;
}
@SuppressFBWarnings(value = {"NP_LOAD_OF_KNOWN_NULL_VALUE", "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"}, justification = "Spotbugs doesn't grok try-with-resources")
private static String loadSymbol(String namespace, String name) {
String markup = PLACEHOLDER_SVG;
ClassLoader classLoader = getClassLoader(namespace);
if (classLoader != null) {
try (InputStream inputStream = classLoader.getResourceAsStream("images/symbols/" + name + ".svg")) {
if (inputStream != null) {
markup = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
} else {
LOGGER.log(Level.FINE, "Missing symbol " + name + " in " + namespace);
}
} catch (IOException e) {
LOGGER.log(Level.FINE, "Failed to load symbol " + name, e);
}
}
return markup.replaceAll("(<title>).*?(</title>)", "$1$2")
.replaceAll("<svg", "<svg aria-hidden=\"true\"")
.replaceAll("(class=\").*?(\")", "")
.replaceAll("(tooltip=\").*?(\")", "")
.replaceAll("(data-html-tooltip=\").*?(\")", "")
.replaceAll("(id=\").*?(\")", "")
.replace("stroke:#000", "stroke:currentColor");
}
@CheckForNull
private static ClassLoader getClassLoader(@NonNull String pluginName) {
if ("core".equals(pluginName)) {
return Symbol.class.getClassLoader();
} else {
PluginWrapper plugin = Jenkins.get().getPluginManager().getPlugin(pluginName);
if (plugin != null) {
return plugin.classLoader;
} else {
return null;
}
}
}
}
package org.jenkins.ui.symbol;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.logging.Logger;
/**
* <p>A Symbol specification, to be passed to {@link Symbol#get(SymbolRequest)}.
*
* <p>Create an instance using {@link Builder}.
*
* @since TODO
*/
public final class SymbolRequest {
private static final Logger LOGGER = Logger.getLogger(SymbolRequest.class.getName());
/**
* The name of the symbol.
*/
@NonNull
private final String name;
/**
* The symbol title.
*/
@CheckForNull
private final String title;
/**
* The tooltip to display when hovering over the symbol. Only displayed if {@link #htmlTooltip} is not set.
*/
@CheckForNull
private final String tooltip;
/**
* An HTML tooltip to display when hovering over the symbol. Overrides any value of the {@link #tooltip} field.
*/
@CheckForNull
private final String htmlTooltip;
/**
* Additional CSS classes to apply to the symbol.
*/
@CheckForNull
private final String classes;
/**
* The name of the plugin to load the symbol from. If null, the symbol will be resolved from core.
*/
@CheckForNull
private final String pluginName;
/**
* The html id of the symbol.
*/
@CheckForNull
private final String id;
@NonNull
public String getName() {
return name;
}
@CheckForNull
public String getTitle() {
return title;
}
@CheckForNull
public String getTooltip() {
return tooltip;
}
@CheckForNull
public String getHtmlTooltip() {
return htmlTooltip;
}
@CheckForNull
public String getClasses() {
return classes;
}
@CheckForNull
public String getPluginName() {
return pluginName;
}
@CheckForNull
public String getId() {
return id;
}
private SymbolRequest(@NonNull String name, @CheckForNull String title, @CheckForNull String tooltip, @CheckForNull String htmlTooltip, @CheckForNull String classes, @CheckForNull String pluginName,
@CheckForNull String id) {
this.name = name;
this.title = title;
this.tooltip = tooltip;
this.htmlTooltip = htmlTooltip;
this.classes = classes;
this.pluginName = pluginName;
this.id = id;
}
public static class Builder {
@CheckForNull
private String name;
@CheckForNull
private String title;
@CheckForNull
private String tooltip;
@CheckForNull
private String htmlTooltip;
@CheckForNull
private String classes;
@CheckForNull
private String pluginName;
@CheckForNull
private String id;
@CheckForNull
private String raw;
@CheckForNull
public String getName() {
return name;
}
public Builder withName(@NonNull String name) {
this.name = name;
return this;
}
@CheckForNull
public String getTitle() {
return title;
}
public Builder withTitle(@CheckForNull String title) {
this.title = title;
return this;
}
@CheckForNull
public String getTooltip() {
return tooltip;
}
public Builder withTooltip(@CheckForNull String tooltip) {
this.tooltip = tooltip;
return this;
}
@CheckForNull
public String getHtmlTooltip() {
return htmlTooltip;
}
public Builder withHtmlTooltip(@CheckForNull String htmlTooltip) {
this.htmlTooltip = htmlTooltip;
return this;
}
@CheckForNull
public String getClasses() {
return classes;
}
public Builder withClasses(@CheckForNull String classes) {
this.classes = classes;
return this;
}
@CheckForNull
public String getPluginName() {
return pluginName;
}
public Builder withPluginName(@CheckForNull String pluginName) {
this.pluginName = pluginName;
return this;
}
@CheckForNull
public String getId() {
return id;
}
public Builder withId(@CheckForNull String id) {
this.id = id;
return this;
}
@CheckForNull
public String getRaw() {
return raw;
}
public Builder withRaw(@CheckForNull String raw) {
this.raw = raw;
return this;
}
@NonNull
public SymbolRequest build() {
if (name == null && pluginName == null && raw != null) {
parseRaw(raw);
LOGGER.fine(() -> "\"" + raw + "\" parsed to name: " + name + " and pluginName: " + pluginName);
}
if (name == null) {
throw new IllegalArgumentException("name cannot be null");
}
return new SymbolRequest(name, title, tooltip, htmlTooltip, classes, pluginName, id);
}
private void parseRaw(@NonNull String raw) {
String[] s = raw.split(" ");
if (s.length <= 2) {
for (String element : s) {
if (element.startsWith("symbol-")) {
name = element.substring("symbol-".length());
}
if (element.startsWith("plugin-")) {
pluginName = element.substring("plugin-".length());
}
if (name != null && pluginName != null) {
break;
}
}
} else {
throw new IllegalArgumentException("raw must be in the format \"symbol-<name> plugin-<pluginName>\"");
}
}
}
}
package org.jenkins.ui.icon;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.not;
......
package org.jenkins.ui.symbol;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class SymbolTest {
public static final String SCIENCE_PATH;
public static final String IMAGES_SYMBOLS_SCIENCE_PATH_XML = "/images/symbols/science.path.xml";
static {
try {
try (InputStream resourceAsStream = SymbolTest.class.getResourceAsStream(IMAGES_SYMBOLS_SCIENCE_PATH_XML)) {
if (resourceAsStream == null) {
throw new IllegalStateException("Could not find resource" + IMAGES_SYMBOLS_SCIENCE_PATH_XML);
}
SCIENCE_PATH = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8).trim();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Test
@DisplayName("Get symbol should build the symbol with given attributes")
void getSymbol() {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTitle("Title")
.withTooltip("Tooltip")
.withClasses("class1 class2")
.withId("id")
.build()
);
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, containsString("<span class=\"jenkins-visually-hidden\">Title</span>"));
assertThat(symbol, containsString("tooltip=\"Tooltip\""));
assertThat(symbol, containsString("class=\"class1 class2\""));
assertThat(symbol, containsString("id=\"id\""));
}
@Test
@DisplayName("HTML tooltip overrides tooltip")
void htmlTooltipOverridesTooltip() {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTooltip("Tooltip")
.withHtmlTooltip("<p>Some HTML Tooltip</p>")
.build()
);
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, not(containsString("tooltip=\"Tooltip\"")));
assertThat(symbol, containsString("data-html-tooltip=\"&lt;p&gt;Some HTML Tooltip&lt;/p&gt;\""));
}
@Test
@DisplayName("Invalid strings should throw IllegalArgumentException")
void invalidRawString() {
assertThrows(IllegalArgumentException.class, () -> new SymbolRequest.Builder().build());
assertThrows(IllegalArgumentException.class, () -> new SymbolRequest.Builder().withRaw("").build());
assertThrows(IllegalArgumentException.class, () -> new SymbolRequest.Builder().withRaw("foobar").build());
assertThrows(IllegalArgumentException.class, () -> new SymbolRequest.Builder().withRaw("plugin-foo").build());
assertThrows(IllegalArgumentException.class, () -> new SymbolRequest.Builder().withRaw("symbol-foo plugin-bar someclass").build());
}
@Test
@DisplayName("Given a raw string it can be parsed to a name")
void rawStringCore() {
SymbolRequest symbol = new SymbolRequest.Builder().withRaw("symbol-gear").build();
assertNull(symbol.getPluginName());
assertEquals("gear", symbol.getName());
}
@Test
@DisplayName("Given a raw string it can be parsed to a name and plugin")
void rawStringPlugin() {
SymbolRequest symbol = new SymbolRequest.Builder().withRaw("symbol-science plugin-foo").build();
assertEquals("foo", symbol.getPluginName());
assertEquals("science", symbol.getName());
}
@Test
@DisplayName("Given a cached symbol, a new request should not return attributes from the cache")
void getSymbol_cachedSymbolDoesntReturnAttributes() {
Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTitle("Title")
.withTooltip("Tooltip")
.withClasses("class1 class2")
.withId("id")
.build()
);
String symbol = Symbol.get(new SymbolRequest.Builder().withName("science").build());
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, not(containsString("<span class=\"jenkins-visually-hidden\">Title</span>")));
assertThat(symbol, not(containsString("tooltip=\"Tooltip\"")));
assertThat(symbol, not(containsString("class=\"class1 class2\"")));
assertThat(symbol, not(containsString("id=\"id\"")));
}
@Test
@DisplayName("Given a cached symbol, a new request can specify new attributes to use")
void getSymbol_cachedSymbolAllowsSettingAllAttributes() {
Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTitle("Title")
.withTooltip("Tooltip")
.withClasses("class1 class2")
.withId("id")
.build()
);
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTitle("Title2")
.withTooltip("Tooltip2")
.withClasses("class3 class4")
.withId("id2")
.build()
);
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, not(containsString("<span class=\"jenkins-visually-hidden\">Title</span>")));
assertThat(symbol, not(containsString("tooltip=\"Tooltip\"")));
assertThat(symbol, not(containsString("class=\"class1 class2\"")));
assertThat(symbol, not(containsString("id=\"id\"")));
assertThat(symbol, containsString("<span class=\"jenkins-visually-hidden\">Title2</span>"));
assertThat(symbol, containsString("tooltip=\"Tooltip2\""));
assertThat(symbol, containsString("class=\"class3 class4\""));
assertThat(symbol, containsString("id=\"id2\""));
}
/**
* YUI tooltips require that the attribute not be set, otherwise a white rectangle will show on hover
* TODO: This might be able to be removed when we move away from YUI tooltips to a better solution
*/
@Test
@DisplayName("When omitting tooltip from attributes, the symbol should not have a tooltip")
void getSymbol_notSettingTooltipDoesntAddTooltipAttribute() {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withTitle("Title")
.withClasses("class1 class2")
.withId("id")
.build()
);
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, not(containsString("tooltip")));
}
@Test
@DisplayName("When resolving a missing symbol, a placeholder is generated instead")
void missingSymbolDefaultsToPlaceholder() {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("missing-icon")
.build()
);
assertThat(symbol, not(containsString(SCIENCE_PATH)));
assertThat(symbol, containsString(Symbol.PLACEHOLDER_MATCHER));
}
@Test
@DisplayName("If tooltip is not provided symbol should never have a tooltip")
void getSymbol_notSettingTooltipDoesntAddTooltipAttribute_evenWithAmpersand() {
SymbolRequest.Builder builder = new SymbolRequest.Builder()
.withName("science")
.withTitle("Title")
.withTooltip("With&Ampersand")
.withClasses("class1 class2")
.withId("id");
String symbol = Symbol.get(builder.build());
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, containsString("tooltip"));
// Remove tooltip
builder.withTooltip(null);
symbol = Symbol.get(builder.build());
assertThat(symbol, containsString(SCIENCE_PATH));
assertThat(symbol, not(containsString("tooltip")));
}
}
<path d="M13,11.33L18,18H6l5-6.67V6h2 M15.96,4H8.04C7.62,4,7.39,4.48,7.65,4.81L9,6.5v4.17L3.2,18.4C2.71,19.06,3.18,20,4,20h16 c0.82,0,1.29-0.94,0.8-1.6L15,10.67V6.5l1.35-1.69C16.61,4.48,16.38,4,15.96,4L15.96,4z"/>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M13,11.33L18,18H6l5-6.67V6h2 M15.96,4H8.04C7.62,4,7.39,4.48,7.65,4.81L9,6.5v4.17L3.2,18.4C2.71,19.06,3.18,20,4,20h16 c0.82,0,1.29-0.94,0.8-1.6L15,10.67V6.5l1.35-1.69C16.61,4.48,16.38,4,15.96,4L15.96,4z"/></g></svg>
\ No newline at end of file
......@@ -267,6 +267,14 @@ THE SOFTWARE.
<outputDirectory>${project.build.outputDirectory}/old-remoting</outputDirectory>
<destFileName>remoting-unsupported.jar</destFileName>
</artifactItem>
<artifactItem>
<groupId>io.jenkins.plugins</groupId>
<artifactId>design-library</artifactId>
<version>91.v257f311ea_1dc</version>
<type>hpi</type>
<outputDirectory>${project.build.outputDirectory}/plugins</outputDirectory>
<destFileName>design-library.jpi</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
......
package org.jenkins.ui.symbol;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.RealJenkinsRule;
public class SymbolJenkinsTest {
@Rule
public RealJenkinsRule rjr = new RealJenkinsRule().addPlugins("plugins/design-library.jpi");
@Test
@DisplayName("When resolving a symbol from a missing plugin, the placeholder is generated instead")
public void missingSymbolFromPluginDefaultsToPlaceholder() throws Throwable {
rjr.then(SymbolJenkinsTest::_missingSymbolFromPluginDefaultsToPlaceholder);
}
private static void _missingSymbolFromPluginDefaultsToPlaceholder(JenkinsRule j) {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("science")
.withPluginName("missing-plugin")
.build()
);
assertThat(symbol, containsString(Symbol.PLACEHOLDER_MATCHER));
}
@Test
@DisplayName("Resolving a valid symbol from an installed plugin does not return the placeholder")
public void resolvingSymbolFromPlugin() throws Throwable {
rjr.then(SymbolJenkinsTest::_resolvingSymbolFromPlugin);
}
private static void _resolvingSymbolFromPlugin(JenkinsRule j) {
String symbol = Symbol.get(new SymbolRequest.Builder()
.withName("app-bar")
.withPluginName("design-library")
.build()
);
assertThat(symbol, not(containsString(Symbol.PLACEHOLDER_MATCHER)));
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册