Real cases · 21 of 25
A hero carousel that renders navigation arrows and indicators but no slides — tracing it back to an inherited Core Component dialog tab that got hidden without anything replacing what it authored, and the contract test that would have caught it pre-launch.
Background reading: Extending a Core Component's Dialog and Hiding Inherited Fields covers the mechanics this post assumes.
QA flagged a hero carousel on a staging page: the carousel shell rendered — left/right arrows, a row of empty indicator dots — but there was nothing to carousel through. No slide content, no error in the browser console, no server-side exception in the logs. The dialog authors used to add slides also had no obvious way to add content anymore; the "Items" tab authors expected simply wasn't there.
The component extended Adobe's Core Component Carousel via sling:resourceSuperType, which is where both halves of the bug lived. A newer variant of the carousel dialog had hidden the inherited children-editor tab (sling:hideResource="{Boolean}true" on a node named to match the Core Component's own "Items" tab), copied from a sibling component where hiding that tab was correct because that sibling managed its content through a multifield instead. On this hero carousel, nothing replaced it — there was no multifield, no alternate mechanism, just an empty tab-shaped hole where the only way to add child slides used to be.
The fix had two parts, and only doing the first one would have left the underlying gap in place for the next copy-paste:
The fix does not touch the Core Component's own carousel JS or HTL — the inherited rendering logic was never broken, it was correctly initializing against zero child resources because zero child resources actually existed. Fixing the symptom at the JS layer (defensive-coding around an empty carousel) would have masked the real problem instead of restoring the missing authoring path.
class HeroCarouselDialogContractTest {
private static Document loadComponentDefinition() throws Exception {
return parse("src/main/content/jcr_root/apps/myapp/components/herocarousel/.content.xml");
}
private static Document loadDialog() throws Exception {
return parse("src/main/content/jcr_root/apps/myapp/components/herocarousel/_cq_dialog/.content.xml");
}
@Test
void hidingTheInheritedChildrenEditorRequiresAReplacementAuthoringPath() throws Exception {
Document componentDefinition = loadComponentDefinition();
String superType = componentDefinition.getDocumentElement()
.getAttribute("sling:resourceSuperType");
assumeTrue(superType.contains("carousel"),
"only applies to components extending a Core Component with a children editor");
Document dialog = loadDialog();
boolean childrenEditorHidden = findHiddenChildrenEditorNode(dialog).isPresent();
boolean hasReplacementAuthoringField = dialogHasMultifieldOrChildResourceField(dialog);
if (childrenEditorHidden) {
assertThat(hasReplacementAuthoringField)
.as("children editor is hidden but no multifield/child-resource field replaces it - "
+ "authors would have no way to add slide content")
.isTrue();
}
}
@Test
void componentWithNoAuthoringPathForChildrenFailsRenderPrecondition() throws Exception {
// A lighter integration check: render the component against a page with
// zero child resources and confirm the model surfaces that as a known,
// named state rather than silently rendering empty navigation controls.
HeroCarouselModel model = context.request(componentResource).adaptTo(HeroCarouselModel.class);
assertThat(model.getSlides()).isEmpty();
assertThat(model.hasAuthorableSlideMechanism())
.as("model should be able to report whether authors have any way to add slides")
.isTrue();
}
}
Dialog fix — restore the inherited tab on this component (delete the local override, letting the merge fall through to the Core Component's own node unmodified):
<!-- BEFORE: copied from a sibling component where this hide was correct -->
<containerItems
jcr:primaryType="nt:unstructured"
jcr:title="Items"
sling:hideResource="{Boolean}true"
sling:resourceType="granite/ui/components/coral/foundation/container">
...
</containerItems>
<!-- AFTER: node removed entirely - the Core Component's own "Items" tab
is no longer overridden, so the merge exposes it unmodified -->
Contract-test helper distinguishing "has a real replacement authoring path" from "has nothing":
final class DialogAuthoringPathInspector {
private DialogAuthoringPathInspector() {
}
static boolean dialogHasMultifieldOrChildResourceField(Document dialog) {
NodeList allNodes = dialog.getElementsByTagName("*");
for (int i = 0; i < allNodes.getLength(); i++) {
Element element = (Element) allNodes.item(i);
String resourceType = element.getAttribute("sling:resourceType");
if (resourceType.contains("foundation/form/multifield")) {
return true;
}
}
return false;
}
static Optional<Element> findHiddenChildrenEditorNode(Document dialog) {
NodeList allNodes = dialog.getElementsByTagName("*");
for (int i = 0; i < allNodes.getLength(); i++) {
Element element = (Element) allNodes.item(i);
boolean isHidden = "true".equalsIgnoreCase(
element.getAttribute("sling:hideResource").replaceAll("[{}Boolean]", ""));
boolean nameLooksLikeChildrenEditor = element.getTagName().toLowerCase(Locale.ROOT)
.contains("containeritems");
if (isHidden && nameLooksLikeChildrenEditor) {
return Optional.of(element);
}
}
return Optional.empty();
}
}
The contract test targets the actual causal chain — "hidden children editor" plus "no replacement" — rather than this one component's name, so it caught a second live instance of the same mistake immediately and will catch future ones automatically as new carousel-extending components get added. Restoring the tab rather than patching around an empty carousel in JS kept the fix at the layer where the actual defect lived: the dialog stopped offering authors a way to do the one thing the component needed them to do.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.