Skip to content

Commit

Permalink
Add optional grouping of features by path in tree view
Browse files Browse the repository at this point in the history
Introduced an optional feature to group feature files by their directory paths in the tree view, providing a better overview of features for teams that organize them by functionality in nested directories.

The default behavior remains unchanged, with features listed without grouping for backward compatibility.

Added new configuration options:
- `groupFeaturesByPath`: Enables or disables path-based grouping (default: false).
- `removableBasePaths`: Specifies base paths to strip from feature file URIs before grouping.
- `directoryNameFormatter`: Customizes how directory names are displayed in the grouped tree view.

Fixes trivago#366
  • Loading branch information
frederikb committed Jan 20, 2025
1 parent 05b9682 commit b711179
Show file tree
Hide file tree
Showing 24 changed files with 1,314 additions and 59 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

Back to [Readme](README.md).

## [3.11.0] - Work In Progress

### Added

* Optional grouping of features by their path in tree view


## [3.10.0] - 2025-01-06

### Added
Expand Down
40 changes: 40 additions & 0 deletions core/src/main/java/com/trivago/cluecumber/core/CluecumberCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.trivago.cluecumber.engine.logging.CluecumberLogger;

import java.util.LinkedHashMap;
import java.util.Set;

/**
* The main Cluecumber core class that passes properties to the Cluecumber engine.
Expand All @@ -48,6 +49,9 @@ private CluecumberCore(final Builder builder) throws CluecumberException {
cluecumberEngine.setCustomStatusColorFailed(builder.customStatusColorFailed);
cluecumberEngine.setCustomStatusColorPassed(builder.customStatusColorPassed);
cluecumberEngine.setCustomStatusColorSkipped(builder.customStatusColorSkipped);
cluecumberEngine.setGroupFeaturesByPath(builder.groupFeaturesByPath);
cluecumberEngine.setRemovableBasePaths(builder.removableBasePaths);
cluecumberEngine.setDirectoryNameFormatter(builder.directoryNameFormatter);
cluecumberEngine.setExpandSubSections(builder.expandSubSections);
cluecumberEngine.setExpandAttachments(builder.expandAttachments);
cluecumberEngine.setExpandBeforeAfterHooks(builder.expandBeforeAfterHooks);
Expand Down Expand Up @@ -86,6 +90,9 @@ public static class Builder {
private String customStatusColorFailed;
private String customStatusColorPassed;
private String customStatusColorSkipped;
private Set<String> removableBasePaths;
private String directoryNameFormatter;
private boolean groupFeaturesByPath;
private boolean expandSubSections;
private boolean expandAttachments;
private boolean expandBeforeAfterHooks;
Expand Down Expand Up @@ -214,6 +221,39 @@ public Builder setCustomStatusColorSkipped(final String customStatusColorSkipped
return this;
}

/**
* Whether to group features by path in the tree view.
*
* @param groupFeaturesByPath If true, the tree view will group features by their directory paths.
* @return The {@link Builder}.
*/
public Builder setGroupFeaturesByPath(final boolean groupFeaturesByPath) {
this.groupFeaturesByPath = groupFeaturesByPath;
return this;
}

/**
* Set the base paths to be removed from feature file URIs before grouping.
*
* @param removableBasePaths A set of strings representing the base paths.
* @return The {@link Builder}.
*/
public Builder setRemovableBasePaths(final Set<String> removableBasePaths) {
this.removableBasePaths = removableBasePaths;
return this;
}

/**
* Set the directory name formatter for customizing directory names.
*
* @param directoryNameFormatter The fully qualified class name of the formatter implementation.
* @return The {@link Builder}.
*/
public Builder setDirectoryNameFormatter(final String directoryNameFormatter) {
this.directoryNameFormatter = directoryNameFormatter;
return this;
}

/**
* Whether to expand subsections or not.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;

import static com.trivago.cluecumber.engine.logging.CluecumberLogger.CluecumberLogLevel.COMPACT;
import static com.trivago.cluecumber.engine.logging.CluecumberLogger.CluecumberLogLevel.DEFAULT;
Expand Down Expand Up @@ -197,6 +198,35 @@ public void setCustomNavigationLinks(final LinkedHashMap<String, String> customN
propertyManager.setCustomNavigationLinks(customNavigationLinks);
}

/**
* Set whether the report should group feature files by their path in the tree view.
*
* @param groupFeaturesByPath true to enable path-based tree view, false otherwise.
*/
public void setGroupFeaturesByPath(final boolean groupFeaturesByPath) {
propertyManager.setGroupFeaturesByPath(groupFeaturesByPath);
}

/**
* Set the base paths to be removed from feature file URIs when used in grouping.
*
* @param removableBasePaths A set of strings representing the base paths.
* @throws WrongOrMissingPropertyException If the paths are invalid.
*/
public void setRemovableBasePaths(final Set<String> removableBasePaths) throws WrongOrMissingPropertyException {
propertyManager.setRemovableBasePaths(removableBasePaths);
}

/**
* Set the directory name formatter.
*
* @param formatterClassName The fully qualified class name of the formatter implementation.
* @throws WrongOrMissingPropertyException Thrown if the class is invalid or missing.
*/
public void setDirectoryNameFormatter(final String formatterClassName) throws WrongOrMissingPropertyException {
propertyManager.setDirectoryNameFormatter(formatterClassName);
}

/**
* Whether to fail scenarios when steps are pending or undefined.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@
import com.trivago.cluecumber.engine.logging.CluecumberLogger;
import com.trivago.cluecumber.engine.rendering.pages.pojos.pagecollections.Link;
import com.trivago.cluecumber.engine.rendering.pages.pojos.pagecollections.LinkType;
import com.trivago.cluecumber.engine.rendering.pages.renderering.BasePaths;
import com.trivago.cluecumber.engine.rendering.pages.renderering.DirectoryNameFormatter;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.trivago.cluecumber.engine.logging.CluecumberLogger.CluecumberLogLevel.COMPACT;
import static com.trivago.cluecumber.engine.logging.CluecumberLogger.CluecumberLogLevel.DEFAULT;
Expand All @@ -52,6 +52,9 @@ public class PropertyManager {
private final Map<String, String> customNavigationLinks = new LinkedHashMap<>();
private String sourceJsonReportDirectory;
private String generatedHtmlReportDirectory;
private DirectoryNameFormatter directoryNameFormatter = new DirectoryNameFormatter.Standard();
private BasePaths basePaths = new BasePaths(Set.of());
private boolean groupFeaturesByPath = false;
private boolean failScenariosOnPendingOrUndefinedSteps = false;
private boolean expandSubSections = false;
private boolean expandBeforeAfterHooks = false;
Expand Down Expand Up @@ -137,6 +140,101 @@ public void setGeneratedHtmlReportDirectory(final String generatedHtmlReportDire
this.generatedHtmlReportDirectory = generatedHtmlReportDirectory;
}

/**
* Gets the {@link BasePaths} instance containing the set of paths which will be removed from feature file URIs before grouping.
*
* @return The {@link BasePaths} instance.
*/
public BasePaths getRemovableBasePaths() {
return basePaths;
}

/**
* Sets the paths which will be removed from feature file URIs when used in grouping.
*
* @param removableBasePaths A set of strings representing the base paths.
* @throws WrongOrMissingPropertyException If the paths are invalid.
*/
public void setRemovableBasePaths(Set<String> removableBasePaths) throws WrongOrMissingPropertyException {
if (removableBasePaths == null) {
return;
}
try {
Set<Path> paths = removableBasePaths.stream()
.map(Path::of)
.collect(Collectors.toSet());
this.basePaths = new BasePaths(paths);
} catch (Exception e) {
logger.warn("Invalid base path(s) provided: " + removableBasePaths);
throw new WrongOrMissingPropertyException("basePaths");
}
}

/**
* Get the currently configured directory name formatter.
*
* @return The {@link DirectoryNameFormatter} instance.
*/
public DirectoryNameFormatter getDirectoryNameFormatter() {
return directoryNameFormatter;
}

/**
* Set the directory name formatter based on the class name of a formatter implementation.
*
* <p>The provided class name must refer to a class that exists, is accessible, and implements the
* {@link DirectoryNameFormatter} interface.
*
* <p>Examples of valid values:
* <ul>
* <li>{@code com.trivago.cluecumber.engine.rendering.pages.renderering.DirectoryNameFormatter$CamelCase}</li>
* <li>{@code com.trivago.cluecumber.engine.rendering.pages.renderering.DirectoryNameFormatter$SnakeCase}</li>
* <li>{@code com.trivago.cluecumber.engine.rendering.pages.renderering.DirectoryNameFormatter$KebabCase}</li>
* </ul>
*
* @param formatterClassName The fully qualified class name of the formatter implementation.
* @throws WrongOrMissingPropertyException Thrown if the class name is invalid or missing.
*/
public void setDirectoryNameFormatter(String formatterClassName) throws WrongOrMissingPropertyException {
if (!isSet(formatterClassName)) {
return;
}
try {
Class<?> clazz = Class.forName(formatterClassName);
if (!DirectoryNameFormatter.class.isAssignableFrom(clazz)) {
logger.warn("The class '" + formatterClassName + "' does not implement DirectoryNameFormatter");
throw new WrongOrMissingPropertyException("directoryNameFormatter");
}
directoryNameFormatter = (DirectoryNameFormatter) clazz.getDeclaredConstructor().newInstance();
} catch (ClassNotFoundException e) {
logger.warn("The class '" + formatterClassName + "' was not found");
throw new WrongOrMissingPropertyException("directoryNameFormatter");
} catch (Exception e) {
logger.warn("An error occurred while setting directoryNameFormatter to '" + formatterClassName + "': " + e.getMessage());
throw new WrongOrMissingPropertyException("directoryNameFormatter");
}
}

/**
* This determines whether the tree view page of feature files will group the features by their URI path.
*
* @return {@code true} if the tree view should use paths to group, {@code false} otherwise.
*/
public boolean isGroupFeaturesByPath() {
return groupFeaturesByPath;
}

/**
* Sets whether the tree view page of feature files will group the features by their URI path.
*
* <p>Setting this to <code>false</code> (the default) keeps the original behavior.
*
* @param groupFeaturesByPath {@code true} to enable grouping by path in the tree view, {@code false} to disable it.
*/
public void setGroupFeaturesByPath(final boolean groupFeaturesByPath) {
this.groupFeaturesByPath = groupFeaturesByPath;
}

/**
* Get the custom parameters to be shown at the top of the report.
*
Expand Down Expand Up @@ -571,6 +669,9 @@ public void logProperties() {
logger.info("- custom parameters display mode : " + customParametersDisplayMode, DEFAULT);
logger.info("- group previous scenario runs : " + groupPreviousScenarioRuns, DEFAULT);
logger.info("- expand previous scenario runs : " + expandPreviousScenarioRuns, DEFAULT);
logger.info("- group features by path : " + groupFeaturesByPath, DEFAULT);
logger.info("- directory name formatter : " + directoryNameFormatter.getClass().getName(), DEFAULT);
logger.info("- removable base paths : " + basePaths.getBasePaths().stream().map(Path::toString).collect(Collectors.joining(", ")), DEFAULT);

if (!customNavigationLinks.isEmpty()) {
customNavigationLinks.entrySet().stream().map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,78 @@

import com.trivago.cluecumber.engine.json.pojo.Element;
import com.trivago.cluecumber.engine.rendering.pages.pojos.Feature;
import com.trivago.cluecumber.engine.rendering.pages.renderering.PathFormatter;

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* Page collection for the tree view page.
*/
public class TreeViewPageCollection extends PageCollection {
private final Map<Feature, List<Element>> elements;
private final Set<Path> paths;
private final Map<Path, List<FeatureAndScenarios>> featuresByPath;
private final PathFormatter pathFormatter;

public static class FeatureAndScenarios {
private final Feature feature;
private final List<Element> scenarios;

public FeatureAndScenarios(Feature feature, List<Element> scenarios) {
this.feature = feature;
this.scenarios = scenarios;
}

public Feature getFeature() {
return feature;
}

public List<Element> getScenarios() {
return scenarios;
}
}

/**
* Constructor.
*
* @param elements The map of features and associated scenarios.
* @param pageTitle The title of the tree view page.
* @param paths The paths of the features.
* @param featuresByPath The map of paths and associated features.
* @param pathFormatter The formatter used to display paths.
* @param pageTitle The title of the tree view page.
*/
public TreeViewPageCollection(final Map<Feature, List<Element>> elements, final String pageTitle) {
public TreeViewPageCollection(final Set<Path> paths, final Map<Path, List<FeatureAndScenarios>> featuresByPath, PathFormatter pathFormatter, final String pageTitle) {
super(pageTitle);
this.elements = elements;
this.paths = paths;
this.featuresByPath = featuresByPath;
this.pathFormatter = pathFormatter;
}

/**
* Get the paths of the features.
*
* @return The paths of the features.
*/
public Set<Path> getPaths() {
return paths;
}

/**
* Get the list of features and their scenarios.
* Get the map of paths and their features.
*
* @return The map of features and associated scenarios.
* @return The map of paths and associated features.
*/
public Map<Path, List<FeatureAndScenarios>> getFeaturesByPath() {
return featuresByPath;
}

/**
* Get the formatter used to turn the path into a displayable title.
* @return The formatter used for paths.
*/
public Map<Feature, List<Element>> getElements() {
return elements;
public PathFormatter getPathFormatter() {
return pathFormatter;
}

/**
Expand All @@ -53,7 +97,7 @@ public Map<Feature, List<Element>> getElements() {
* @return The count.
*/
public int getNumberOfFeatures() {
return elements.size();
return featuresByPath.values().stream().mapToInt(List::size).sum();
}

/**
Expand All @@ -62,7 +106,7 @@ public int getNumberOfFeatures() {
* @return The count.
*/
public int getNumberOfScenarios() {
return elements.values().stream().mapToInt(List::size).sum();
return featuresByPath.values().stream().flatMap(List::stream).map(FeatureAndScenarios::getScenarios).mapToInt(List::size).sum();
}
}

Loading

0 comments on commit b711179

Please sign in to comment.