You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lo...@apache.org on 2020/10/23 18:39:28 UTC

[myfaces-tobago] 02/13: TOBAGO-1633: Use TypeScript instead of JavaScript. Simplify Renderers

This is an automated email from the ASF dual-hosted git repository.

lofwyr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit b1fe246fdc1698c97dfbc4a79ce9cfab295c4f56
Author: Udo Schnurpfeil <ud...@irian.eu>
AuthorDate: Thu Oct 22 12:26:25 2020 +0200

    TOBAGO-1633: Use TypeScript instead of JavaScript.
    Simplify Renderers
---
 .../myfaces/tobago/component/RendererTypes.java    |  2 -
 .../apache/myfaces/tobago/config/TobagoConfig.java | 17 ++++-
 .../renderkit/renderer/CommandRendererBase.java    | 14 +++-
 .../renderer/LinkInsideLinksRenderer.java          | 67 ------------------
 .../internal/renderkit/renderer/LinksRenderer.java | 17 ++---
 .../internal/config/AbstractTobagoTestBase.java    | 13 +++-
 .../renderkit/renderer/InRendererUnitTest.java     | 39 +----------
 .../renderkit/renderer/LinkRendererUnitTest.java   | 79 ++++++++++++++++++++++
 .../renderkit/renderer/RendererTestBase.java       | 63 +++++++++++++++++
 .../renderer/link/link-inside-links-sub.html       | 32 +++++++++
 .../resources/renderer/link/link-inside-links.html | 26 +++++++
 .../src/test/resources/renderer/link/link.html     | 20 ++++++
 12 files changed, 266 insertions(+), 123 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
index b4cf843..f97e451 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/RendererTypes.java
@@ -51,7 +51,6 @@ public enum RendererTypes {
   Image,
   Link,
   LinkInsideCommand,
-  LinkInsideLinks,
   Links,
   Label,
   Messages,
@@ -136,7 +135,6 @@ public enum RendererTypes {
   public static final String IMAGE = "Image";
   public static final String LINK = "Link";
   public static final String LINK_INSIDE_COMMAND = "LinkInsideCommand";
-  public static final String LINK_INSIDE_LINKS = "LinkInsideLinks";
   public static final String LINKS = "Links";
   public static final String LABEL = "Label";
   public static final String MESSAGES = "Messages";
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/config/TobagoConfig.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/config/TobagoConfig.java
index 4374d13..e7d0594 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/config/TobagoConfig.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/config/TobagoConfig.java
@@ -23,6 +23,8 @@ import org.apache.myfaces.tobago.context.Theme;
 import org.apache.myfaces.tobago.internal.config.ContentSecurityPolicy;
 import org.apache.myfaces.tobago.internal.config.SecurityAnnotation;
 import org.apache.myfaces.tobago.sanitizer.Sanitizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.enterprise.inject.spi.CDI;
 import javax.faces.context.FacesContext;
@@ -32,6 +34,8 @@ import java.util.Map;
 
 public abstract class TobagoConfig {
 
+  private static final Logger LOG = LoggerFactory.getLogger(TobagoConfig.class);
+
   /**
    * @deprecated Since 5.0.0. Please use CDI.
    */
@@ -43,7 +47,18 @@ public abstract class TobagoConfig {
    */
   @Deprecated
   public static TobagoConfig getInstance(final FacesContext facesContext) {
-    return CDI.current().select(TobagoConfig.class).get();
+    TobagoConfig tobagoConfig = null;
+    try {
+      tobagoConfig = CDI.current().select(TobagoConfig.class).get();
+    } catch (Exception e) {
+      LOG.warn("No CDI!");
+    }
+    if (tobagoConfig != null) {
+      return tobagoConfig;
+    } else {
+      // XXX not nice: this happens while unit tests and whenever???
+      return (TobagoConfig) facesContext.getExternalContext().getApplicationMap().get(TOBAGO_CONFIG);
+    }
   }
 
   /**
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
index 7b69c2b..38c5f4b 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/CommandRendererBase.java
@@ -120,7 +120,8 @@ public abstract class CommandRendererBase<T extends AbstractUICommand> extends D
         parentOfCommands ? null : getOuterCssItems(facesContext, component),
         getCssItems(facesContext, component),
         parentOfCommands && !dropdownSubmenu ? BootstrapClass.DROPDOWN_TOGGLE : null,
-        component.getCustomClass());
+        component.getCustomClass(),
+        isInside(facesContext, HtmlElements.TOBAGO_LINKS) ? BootstrapClass.NAV_LINK : null);
 
     final boolean defaultCommand = ComponentUtils.getBooleanAttribute(component, Attributes.defaultCommand);
     if (defaultCommand) {
@@ -228,6 +229,7 @@ public abstract class CommandRendererBase<T extends AbstractUICommand> extends D
   }
 
   protected void encodeBeginOuter(final FacesContext facesContext, final T command) throws IOException {
+
     final String clientId = command.getClientId(facesContext);
     final boolean parentOfCommands = command.isParentOfCommands();
     final boolean dropdownSubmenu = this instanceof LinkInsideCommandRenderer;
@@ -239,8 +241,16 @@ public abstract class CommandRendererBase<T extends AbstractUICommand> extends D
       writer.startElement(HtmlElements.TOBAGO_DROPDOWN);
       writer.writeIdAttribute(clientId);
 
+      final CssItem first;
+      if (childOfButtonGroup) {
+        first = null;
+      } else if (dropdownSubmenu) {
+        first = TobagoClass.DROPDOWN__SUBMENU;
+      } else {
+        first = BootstrapClass.DROPDOWN;
+      }
       writer.writeClassAttribute(
-          childOfButtonGroup ? null : dropdownSubmenu ? TobagoClass.DROPDOWN__SUBMENU : BootstrapClass.DROPDOWN,
+          first,
           getOuterCssItems(facesContext, command));
     }
   }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java
deleted file mode 100644
index f33cc73..0000000
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkInsideLinksRenderer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF 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.apache.myfaces.tobago.internal.renderkit.renderer;
-
-import org.apache.myfaces.tobago.internal.component.AbstractUILink;
-import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
-import org.apache.myfaces.tobago.renderkit.css.CssItem;
-import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
-import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
-
-import javax.faces.context.FacesContext;
-import java.io.IOException;
-
-public class LinkInsideLinksRenderer<T extends AbstractUILink> extends LinkRenderer<T> {
-
-  @Override
-  protected void encodeBeginOuter(final FacesContext facesContext, final T command) throws IOException {
-    final String clientId = command.getClientId(facesContext);
-    final boolean parentOfCommands = command.isParentOfCommands();
-
-    final TobagoResponseWriter writer = getResponseWriter(facesContext);
-
-    writer.startElement(HtmlElements.LI);
-    if (parentOfCommands) {
-      writer.startElement(HtmlElements.TOBAGO_DROPDOWN);
-      writer.writeIdAttribute(clientId);
-    }
-
-    writer.writeClassAttribute(
-        BootstrapClass.NAV_ITEM,
-        parentOfCommands ? BootstrapClass.DROPDOWN : null);
-  }
-
-  @Override
-  protected void encodeEndOuter(final FacesContext facesContext, final T command) throws IOException {
-    final boolean parentOfCommands = command.isParentOfCommands();
-
-    final TobagoResponseWriter writer = getResponseWriter(facesContext);
-
-    if (parentOfCommands) {
-      writer.endElement(HtmlElements.TOBAGO_DROPDOWN);
-    }
-    writer.endElement(HtmlElements.LI);
-  }
-
-  @Override
-  protected CssItem[] getCssItems(final FacesContext facesContext, final T command) {
-    return new CssItem[]{BootstrapClass.NAV_LINK};
-  }
-}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinksRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinksRenderer.java
index 2f775fd..bb0423f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinksRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinksRenderer.java
@@ -19,8 +19,6 @@
 
 package org.apache.myfaces.tobago.internal.renderkit.renderer;
 
-import org.apache.myfaces.tobago.component.RendererTypes;
-import org.apache.myfaces.tobago.internal.component.AbstractUILink;
 import org.apache.myfaces.tobago.internal.component.AbstractUILinks;
 import org.apache.myfaces.tobago.layout.Orientation;
 import org.apache.myfaces.tobago.renderkit.RendererBase;
@@ -61,19 +59,16 @@ public class LinksRenderer<T extends AbstractUILinks> extends RendererBase<T> {
   public void encodeChildrenInternal(final FacesContext facesContext, final T component) throws IOException {
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
 
+    insideBegin(facesContext, HtmlElements.TOBAGO_LINKS);
     for (final UIComponent child : component.getChildren()) {
       if (child.isRendered()) {
-        if (child instanceof AbstractUILink) {
-          child.setRendererType(RendererTypes.LinkInsideLinks.name());
-          child.encodeAll(facesContext);
-        } else {
-          writer.startElement(HtmlElements.LI);
-          writer.writeClassAttribute(BootstrapClass.NAV_ITEM);
-          child.encodeAll(facesContext);
-          writer.endElement(HtmlElements.LI);
-        }
+        writer.startElement(HtmlElements.LI);
+        writer.writeClassAttribute(BootstrapClass.NAV_ITEM);
+        child.encodeAll(facesContext);
+        writer.endElement(HtmlElements.LI);
       }
     }
+    insideEnd(facesContext, HtmlElements.TOBAGO_LINKS);
   }
 
   @Override
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
index ea63aa2..fbb0d5f 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
@@ -26,20 +26,23 @@ import org.apache.myfaces.test.mock.MockHttpServletRequest;
 import org.apache.myfaces.tobago.component.RendererTypes;
 import org.apache.myfaces.tobago.component.Tags;
 import org.apache.myfaces.tobago.component.UIButton;
+import org.apache.myfaces.tobago.component.UIButtons;
 import org.apache.myfaces.tobago.component.UIGridLayout;
 import org.apache.myfaces.tobago.component.UIIn;
 import org.apache.myfaces.tobago.component.UILink;
+import org.apache.myfaces.tobago.component.UILinks;
 import org.apache.myfaces.tobago.component.UIOut;
 import org.apache.myfaces.tobago.component.UIPanel;
 import org.apache.myfaces.tobago.component.UIPopup;
 import org.apache.myfaces.tobago.component.UISegmentLayout;
 import org.apache.myfaces.tobago.component.UIStyle;
-import org.apache.myfaces.tobago.config.TobagoConfig;
 import org.apache.myfaces.tobago.context.TobagoContext;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.ButtonRenderer;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.ButtonsRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.GridLayoutRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.InRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.LinkRenderer;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.LinksRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.OutRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.PanelRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.PopupRenderer;
@@ -55,6 +58,7 @@ import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Locale;
 
+import static org.apache.myfaces.tobago.config.TobagoConfig.TOBAGO_CONFIG;
 import static org.apache.myfaces.tobago.util.ResourceUtils.TOBAGO_RESOURCE_BUNDLE;
 
 /**
@@ -87,7 +91,8 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     // Tobago specific extensions
     final TobagoConfigImpl tobagoConfig = TobagoConfigMergingUnitTest.load("tobago-config-for-unit-tests.xml");
     tobagoConfig.initDefaultValidatorInfo();
-    servletContext.setAttribute(TobagoConfig.TOBAGO_CONFIG, tobagoConfig);
+    servletContext.setAttribute(TOBAGO_CONFIG, tobagoConfig);
+    facesContext.getExternalContext().getApplicationMap().put(TOBAGO_CONFIG, tobagoConfig);
 
     final TobagoContext tobagoContext = new TobagoContext();
     tobagoContext.setTheme(tobagoConfig.getDefaultTheme());
@@ -99,7 +104,9 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     application.addComponent(Tags.out.componentType(), UIOut.class.getName());
     application.addComponent(Tags.panel.componentType(), UIPanel.class.getName());
     application.addComponent(Tags.link.componentType(), UILink.class.getName());
+    application.addComponent(Tags.links.componentType(), UILinks.class.getName());
     application.addComponent(Tags.button.componentType(), UIButton.class.getName());
+    application.addComponent(Tags.buttons.componentType(), UIButtons.class.getName());
     application.addComponent(Tags.popup.componentType(), UIPopup.class.getName());
     application.addComponent(Tags.style.componentType(), UIStyle.class.getName());
     application.addComponent(Tags.gridLayout.componentType(), UIGridLayout.class.getName());
@@ -110,7 +117,9 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     renderKit.addRenderer(UIOut.COMPONENT_FAMILY, RendererTypes.OUT, new OutRenderer());
     renderKit.addRenderer(UIPanel.COMPONENT_FAMILY, RendererTypes.PANEL, new PanelRenderer());
     renderKit.addRenderer(UILink.COMPONENT_FAMILY, RendererTypes.LINK, new LinkRenderer());
+    renderKit.addRenderer(UILinks.COMPONENT_FAMILY, RendererTypes.LINKS, new LinksRenderer());
     renderKit.addRenderer(UIButton.COMPONENT_FAMILY, RendererTypes.BUTTON, new ButtonRenderer());
+    renderKit.addRenderer(UIButtons.COMPONENT_FAMILY, RendererTypes.BUTTONS, new ButtonsRenderer());
     renderKit.addRenderer(UIPopup.COMPONENT_FAMILY, RendererTypes.POPUP, new PopupRenderer());
     renderKit.addRenderer(UIStyle.COMPONENT_FAMILY, RendererTypes.STYLE, new StyleRenderer());
     renderKit.addRenderer(UIGridLayout.COMPONENT_FAMILY, RendererTypes.GRID_LAYOUT, new GridLayoutRenderer());
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRendererUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRendererUnitTest.java
index 5b999b6..f78cf7f 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRendererUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRendererUnitTest.java
@@ -24,20 +24,14 @@ import org.apache.myfaces.tobago.component.RendererTypes;
 import org.apache.myfaces.tobago.component.Tags;
 import org.apache.myfaces.tobago.component.UIIn;
 import org.apache.myfaces.tobago.component.UISegmentLayout;
-import org.apache.myfaces.tobago.internal.config.AbstractTobagoTestBase;
 import org.apache.myfaces.tobago.layout.SegmentMeasureList;
 import org.apache.myfaces.tobago.util.ComponentUtils;
 import org.junit.Assert;
 import org.junit.jupiter.api.Test;
 
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.stream.Collectors;
 
-public class InRendererUnitTest extends AbstractTobagoTestBase {
+public class InRendererUnitTest extends RendererTestBase {
 
   @Test
   public void simple() throws IOException {
@@ -204,35 +198,4 @@ public class InRendererUnitTest extends AbstractTobagoTestBase {
 
     Assert.assertEquals(loadHtml("renderer/in/label-none.html"), formattedResult());
   }
-
-  private String formattedResult() throws IOException {
-    return format1To2Indent(getLastWritten());
-  }
-
-  private String loadHtml(final String fileName) throws IOException {
-    final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
-    try (InputStream is = classLoader.getResourceAsStream(fileName)) {
-      if (is == null) {
-        throw new FileNotFoundException(fileName);
-      }
-      try (final InputStreamReader isr = new InputStreamReader(is);
-           final BufferedReader reader = new BufferedReader(isr)) {
-        return reader.lines().collect(Collectors.joining(System.lineSeparator()))
-            .replaceAll("<!--[^>]*-->", "")
-            .replaceAll("^\n\n", "");
-      }
-    }
-  }
-
-  private String format1To2Indent(final String xml) {
-    return xml.replaceAll("^\n", "")
-    .replaceAll("\n <", "\n\t<")
-    .replaceAll("\n  <", "\n\t\t<")
-    .replaceAll("\n   <", "\n\t\t\t<")
-    .replaceAll("\n    <", "\n\t\t\t\t<")
-    .replaceAll("\n     <", "\n\t\t\t\t\t<")
-    .replaceAll("\n      <", "\n\t\t\t\t\t\t<")
-    .replaceAll("\n       <", "\n\t\t\t\t\t\t\t<")
-        .replaceAll("\t", "  ");
-  }
 }
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkRendererUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkRendererUnitTest.java
new file mode 100644
index 0000000..d4df468
--- /dev/null
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/LinkRendererUnitTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.myfaces.tobago.internal.renderkit.renderer;
+
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.component.Tags;
+import org.apache.myfaces.tobago.component.UILink;
+import org.apache.myfaces.tobago.component.UILinks;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+public class LinkRendererUnitTest extends RendererTestBase {
+
+  @Test
+  public void link() throws IOException {
+    final UILink c = (UILink) ComponentUtils.createComponent(
+        facesContext, Tags.link.componentType(), RendererTypes.Link, "id");
+    c.setLabel("label");
+    c.setLink("https://www.apache.org/");
+    c.encodeAll(facesContext);
+
+    Assert.assertEquals(loadHtml("renderer/link/link.html"), formattedResult());
+  }
+
+  @Test
+  public void linkInsideLinks() throws IOException {
+    final UILinks l = (UILinks) ComponentUtils.createComponent(
+        facesContext, Tags.links.componentType(), RendererTypes.Links, "list");
+    final UILink c = (UILink) ComponentUtils.createComponent(
+        facesContext, Tags.link.componentType(), RendererTypes.Link, "id");
+    c.setLabel("apache");
+    c.setLink("https://www.apache.org/");
+    l.getChildren().add(c);
+    l.encodeAll(facesContext);
+
+    Assert.assertEquals(loadHtml("renderer/link/link-inside-links.html"), formattedResult());
+  }
+
+  @Test
+  public void linkInsideLinksSub() throws IOException {
+    final UILinks l = (UILinks) ComponentUtils.createComponent(
+        facesContext, Tags.links.componentType(), RendererTypes.Links, "list");
+    final UILink c = (UILink) ComponentUtils.createComponent(
+        facesContext, Tags.link.componentType(), RendererTypes.Link, "id");
+    c.setLabel("apache");
+
+    final UILink s = (UILink) ComponentUtils.createComponent(
+        facesContext, Tags.link.componentType(), RendererTypes.Link, "sub");
+    s.setLabel("sub");
+    s.setLink("https://www.apache.org/");
+    c.getChildren().add(s);
+
+    l.getChildren().add(c);
+    l.encodeAll(facesContext);
+
+    Assert.assertEquals(loadHtml("renderer/link/link-inside-links-sub.html"), formattedResult());
+  }
+
+}
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/RendererTestBase.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/RendererTestBase.java
new file mode 100644
index 0000000..c1c72ce
--- /dev/null
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/RendererTestBase.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.myfaces.tobago.internal.renderkit.renderer;
+
+import org.apache.myfaces.tobago.internal.config.AbstractTobagoTestBase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+
+public abstract class RendererTestBase extends AbstractTobagoTestBase {
+
+  protected String formattedResult() throws IOException {
+    return format1To2Indent(getLastWritten());
+  }
+
+  protected String loadHtml(final String fileName) throws IOException {
+    final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+    try (InputStream is = classLoader.getResourceAsStream(fileName)) {
+      if (is == null) {
+        throw new FileNotFoundException(fileName);
+      }
+      try (final InputStreamReader isr = new InputStreamReader(is);
+           final BufferedReader reader = new BufferedReader(isr)) {
+        return reader.lines().collect(Collectors.joining(System.lineSeparator()))
+            .replaceAll("<!--[^>]*-->", "")
+            .replaceAll("^\n\n", "");
+      }
+    }
+  }
+
+  protected String format1To2Indent(final String xml) {
+    return xml.replaceAll("^\n", "")
+    .replaceAll("\n <", "\n\t<")
+    .replaceAll("\n  <", "\n\t\t<")
+    .replaceAll("\n   <", "\n\t\t\t<")
+    .replaceAll("\n    <", "\n\t\t\t\t<")
+    .replaceAll("\n     <", "\n\t\t\t\t\t<")
+    .replaceAll("\n      <", "\n\t\t\t\t\t\t<")
+    .replaceAll("\n       <", "\n\t\t\t\t\t\t\t<")
+        .replaceAll("\t", "  ");
+  }
+}
diff --git a/tobago-core/src/test/resources/renderer/link/link-inside-links-sub.html b/tobago-core/src/test/resources/renderer/link/link-inside-links-sub.html
new file mode 100644
index 0000000..fc95c8e
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/link/link-inside-links-sub.html
@@ -0,0 +1,32 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+-->
+<!--
+CSS class nav-item has moved from "tobago-dropdown" to "li"
+-->
+<tobago-links id='list'>
+  <ul class='nav'>
+    <li class='nav-item'>
+      <tobago-dropdown id='id' class='dropdown'>
+        <button type='button' id='id::command' name='id' data-toggle='dropdown' class='tobago-link dropdown-toggle nav-link'>
+          <tobago-behavior event='click' decoupled='decoupled'>
+          </tobago-behavior><span>apache</span></button>
+        <div class='dropdown-menu' aria-labelledby='id::command' name='id'>
+        </div>
+      </tobago-dropdown>
+    </li>
+  </ul>
+</tobago-links>
\ No newline at end of file
diff --git a/tobago-core/src/test/resources/renderer/link/link-inside-links.html b/tobago-core/src/test/resources/renderer/link/link-inside-links.html
new file mode 100644
index 0000000..c3fca1d
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/link/link-inside-links.html
@@ -0,0 +1,26 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+-->
+
+<tobago-links id='list'>
+  <ul class='nav'>
+    <li class='nav-item'>
+      <a id='id' name='id' href='https://www.apache.org/' class='tobago-link nav-link'>
+        <tobago-behavior event='click' omit='omit' decoupled='decoupled'>
+        </tobago-behavior><span>apache</span></a>
+    </li>
+  </ul>
+</tobago-links>
\ No newline at end of file
diff --git a/tobago-core/src/test/resources/renderer/link/link.html b/tobago-core/src/test/resources/renderer/link/link.html
new file mode 100644
index 0000000..a327675
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/link/link.html
@@ -0,0 +1,20 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.
+-->
+
+<a id='id' name='id' href='https://www.apache.org/' class='tobago-link'>
+  <tobago-behavior event='click' omit='omit' decoupled='decoupled'>
+  </tobago-behavior><span>label</span></a>
\ No newline at end of file