Skip to content

Commit

Permalink
Specs to demonstrate requirements aggregation with Serenity/JS
Browse files Browse the repository at this point in the history
  • Loading branch information
jan-molak committed Dec 4, 2023
1 parent a51c4e7 commit f54b35e
Show file tree
Hide file tree
Showing 8 changed files with 395 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ public class FileSystemRequirementsTagProvider extends AbstractRequirementsTagPr
private final RequirementsConfiguration requirementsConfiguration;
private volatile List<Requirement> requirements;

private final static String JAVASCRIPT_SPEC_FILE_EXTENSION_PATTERN =
"\\.(spec|test|integration|it|e2e|spec\\.e2e|spec-e2e)" + // Consider only test files...
"\\.(jsx?|mjsx?|cjsx?|tsx?|mtsx?|ctsx?)$"; // implemented in either JavaScript or TypeScript

private final static String JAVASCRIPT_SPEC_FILE_NAME_PATTERN =
"^(?!.*/(node_modules|jspm_packages|web_modules)/)" + // Ignore external dependencies
".*" +
JAVASCRIPT_SPEC_FILE_EXTENSION_PATTERN;

public FileSystemRequirementsTagProvider(EnvironmentVariables environmentVariables) {
this(environmentVariables,
RootDirectory.definedIn(environmentVariables).featuresOrStoriesRootDirectory().orElse(defaultFeatureDirectory()).toString());
Expand Down Expand Up @@ -180,14 +189,15 @@ private Map<PathElements, Requirement> indexByPath(List<Requirement> requirement

private Stream<Requirement> capabilitiesAndStoriesIn(String path) {
File rootDirectory = new File(path);
if (rootDirectory.exists()) {
return Stream.concat(
loadCapabilitiesFrom(rootDirectory.listFiles(thatAreFeatureDirectories())),
loadStoriesFrom(rootDirectory.listFiles(thatAreStories()))
);
} else {

if (! (rootDirectory.exists() && rootDirectory.isDirectory())) {
return NO_REQUIREMENTS.stream();
}

return Stream.concat(
loadCapabilitiesFrom(rootDirectory.listFiles(thatAreFeatureOrSpecDirectories())),
loadStoriesFrom(rootDirectory.listFiles(thatAreStories()))
);
}

private int maxDirectoryDepthIn(Set<String> directoryPaths) {
Expand Down Expand Up @@ -546,7 +556,6 @@ private Stream<Requirement> loadCapabilitiesFrom(File[] requirementDirectories)
return Arrays.stream(requirementDirectories).map(this::readRequirementFrom);
}


private Stream<Requirement> loadStoriesFrom(File[] storyFiles) {
return Arrays.stream(storyFiles)
.map(this::readRequirementsFromStoryOrFeatureFile)
Expand Down Expand Up @@ -814,13 +823,10 @@ private List<Requirement> readChildrenFrom(File requirementDirectory) {
}

private boolean childrenExistFor(String path) {
if (hasSubdirectories(path)) {
return true;
} else if (hasFeatureOrStoryFiles(path)) {
return true;
} else {
return classpathResourceExistsFor(path);
}
return hasSubdirectories(path)
|| hasFeatureOrStoryFiles(path)
|| hasJavaScriptSpecFiles(path)
|| classpathResourceExistsFor(path);
}

private boolean hasFeatureOrStoryFiles(String path) {
Expand All @@ -832,6 +838,12 @@ private boolean hasFeatureOrStoryFiles(String path) {
}
}

private boolean hasJavaScriptSpecFiles(String path) {
File requirementDirectory = new File(path);
return requirementDirectory.isDirectory()
&& requirementDirectory.list(javaScriptSpecFiles()).length > 0;
}

private FilenameFilter storyFiles() {
return (dir, name) -> name.endsWith(".story");
}
Expand All @@ -840,6 +852,10 @@ private FilenameFilter featureFiles() {
return (dir, name) -> name.endsWith(".feature");
}

private FilenameFilter javaScriptSpecFiles() {
return (dir, name) -> name.matches(JAVASCRIPT_SPEC_FILE_NAME_PATTERN);
}

private boolean classpathResourceExistsFor(String path) {
return getClass().getResource(resourcePathFor(path)) != null;
}
Expand Down Expand Up @@ -868,12 +884,18 @@ private String getTitleFromNarrativeOrDirectoryName(RequirementDefinition requir
return nameIfNoNarrativePresent;
}

private FileFilter thatAreFeatureDirectories() {
return file -> !file.getName().startsWith(".") && storyOrFeatureFilesExistIn(file);
private FileFilter thatAreFeatureOrSpecDirectories() {
return file -> file.isDirectory()
&& !file.getName().startsWith(".")
&& storyFeatureFilesOrSpecsExistIn(file);
}

private boolean storyOrFeatureFilesExistIn(File directory) {
return startingAt(normalised(directory)).containsFiles(thatAreStories(), thatAreNarratives());
private boolean storyFeatureFilesOrSpecsExistIn(File directory) {
return startingAt(normalised(directory)).containsFiles(
thatAreStories(),
thatAreNarratives(),
thatAreJavaScriptSpecFiles()
);
}

private FileFilter thatAreStories() {
Expand All @@ -894,6 +916,12 @@ private FileFilter thatAreNarratives() {
|| file.getName().equalsIgnoreCase("placeholder.txt");
}

private FileFilter thatAreJavaScriptSpecFiles() {
return file -> file.getName().matches(
JAVASCRIPT_SPEC_FILE_NAME_PATTERN
);
}

private boolean isSupportedFileStoryExtension(String storyFileExtension) {
return (storyFileExtension.equalsIgnoreCase(FEATURE_EXTENSION) || storyFileExtension.equalsIgnoreCase(STORY_EXTENSION));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package net.thucydides.model.requirements;

import net.thucydides.model.domain.RequirementCache;
import net.thucydides.model.requirements.model.Requirement;
import org.junit.jupiter.api.*;

import java.io.File;
import java.nio.file.Path;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class AggregateRequirementsTest {

@BeforeEach
void setUp() {
RequirementCache.getInstance().clear();
}

@AfterAll
static void afterAll() {
RequirementCache.getInstance().clear();
}

@Nested
@DisplayName("when interpreting Serenity/JS test outcomes")
class SerenityJSTestOutcomes {

@Test
void should_treat_files_in_a_flat_directory_structure_as_representing_features() {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-flat"));

assertThat(requirements).hasSize(1);

Requirement feature = requirements.get(0);

assertThat(feature.getType()).isEqualTo("feature");
assertThat(feature.getName()).isEqualTo("card_payment");
assertThat(feature.getDisplayName()).isEqualTo("Card payment");
}

@Test
void should_treat_files_in_a_single_level_directory_structure_as_representing_capabilities_and_features() {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-1-level"));

assertThat(requirements).hasSize(2);

Requirement feature = requirements.get(0);
Requirement capability = requirements.get(1);

assertThat(feature.getType()).isEqualTo("feature");
assertThat(feature.getName()).isEqualTo("card_payment");
assertThat(feature.getDisplayName()).isEqualTo("Card Payment");

assertThat(capability.getType()).isEqualTo("capability");
assertThat(capability.getName()).isEqualTo("payments");
assertThat(capability.getDisplayName()).isEqualTo("Payments");
}

@Test
void should_treat_files_in_a_two_level_directory_structure_as_representing_themes_capabilities_and_features() {

List<Requirement> requirements = requirementsFrom(pathTo("serenity-js/spec-2-levels"));

assertThat(requirements).hasSize(3);

Requirement feature = requirements.get(0);
Requirement capability = requirements.get(1);
Requirement theme = requirements.get(2);

assertThat(feature.getType()).isEqualTo("feature");
assertThat(feature.getName()).isEqualTo("card_payment");
assertThat(feature.getDisplayName()).isEqualTo("Card Payment");

assertThat(capability.getType()).isEqualTo("capability");
assertThat(capability.getName()).isEqualTo("payments");
assertThat(capability.getDisplayName()).isEqualTo("Payments");

assertThat(theme.getType()).isEqualTo("theme");
assertThat(theme.getName()).isEqualTo("ecommerce");
assertThat(theme.getDisplayName()).isEqualTo("Ecommerce");
}
}

private List<Requirement> requirementsFrom(Path exampleRootDirectory) {
Path requirementsDirectory = exampleRootDirectory.resolve("spec");
Path jsonOutcomesDirectory = exampleRootDirectory.resolve("outcomes");

final AggregateRequirements aggregateRequirements = new AggregateRequirements(jsonOutcomesDirectory, requirementsDirectory.toString());
final RequirementsService service = aggregateRequirements.getRequirementsService();

return service.getRequirements();
}

private static Path pathTo(String resource) {
return new File(ClassLoader.getSystemClassLoader().getResource(resource).getFile()).toPath();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"id": "checkout-should-allow-the-customer-to-pick-their-preferred-card;macos-23-1-0;chromium-120-0-6099-28",
"name": "Checkout should allow the customer to pick their preferred card",
"title": "Checkout should allow the customer to pick their preferred card",
"manual": false,
"testSteps": [
{
"number": 1,
"description": "Tess starts with an empty basket",
"startTime": 1701531998839,
"children": [],
"reportData": [],
"screenshots": [],
"duration": 1897,
"result": "SUCCESS"
}
],
"userStory": {
"id": "card-payment",
"storyName": "Card payment",
"displayName": "Card payment",
"path": "payments/card_payment",
"type": "feature",
"narrative": "",
"pathElements": [
{
"name": "payments",
"description": ""
},
{
"name": "card_payment",
"description": ""
}
]
},
"startTime": "2023-12-02T15:46:38.235Z",
"tags": [
{
"name": "payments",
"type": "capability",
"displayName": "Payments"
},
{
"name": "payments/Card payment",
"type": "feature",
"displayName": "Card payment"
},
{
"name": "macOS 23.1.0",
"type": "platform",
"platformName": "macOS",
"platformVersion": "23.1.0",
"displayName": "macOS 23.1.0"
},
{
"name": "chromium 120.0.6099.28",
"type": "browser",
"browserName": "chromium",
"browserVersion": "120.0.6099.28",
"displayName": "chromium 120.0.6099.28"
}
],
"featureTag": {
"name": "payments/Card payment",
"type": "feature",
"displayName": "Card payment"
},
"testSource": "JS",
"context": "mac,chrome",
"driver": "chromium",
"result": "SUCCESS",
"duration": 3499
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, it } from '@serenity-js/playwright-test'

describe('Card payment', () => {

describe('Checkout', () => {
it('should allow the customer to pick their preferred card', async ({ actor }) => {
// ...
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"id": "card_payment;checkout-should-allow-the-customer-to-pick-their-preferred-card;macos-23-1-0;chromium-120-0-6099-28",
"name": "Checkout should allow the customer to pick their preferred card",
"title": "Checkout should allow the customer to pick their preferred card",
"manual": false,
"testSteps": [
{
"number": 1,
"description": "Tess starts with an empty basket",
"startTime": 1701531998839,
"children": [],
"reportData": [],
"screenshots": [],
"duration": 1897,
"result": "SUCCESS"
}
],
"userStory": {
"id": "card-payment",
"storyName": "Card payment",
"displayName": "Card payment",
"path": "ecommerce/payments/card_payment",
"type": "feature",
"narrative": "",
"pathElements": [
{
"name": "ecommerce",
"description": ""
},
{
"name": "payments",
"description": ""
},
{
"name": "card_payment",
"description": ""
}
]
},
"startTime": "2023-12-02T15:46:38.235Z",
"tags": [
{
"name": "ecommerce",
"type": "theme",
"displayName": "ecommerce"
},
{
"name": "ecommerce/payments",
"type": "capability",
"displayName": "payments"
},
{
"name": "payments/Card payment",
"type": "feature",
"displayName": "Card payment"
},
{
"name": "macOS 23.1.0",
"type": "platform",
"platformName": "macOS",
"platformVersion": "23.1.0",
"displayName": "macOS 23.1.0"
},
{
"name": "chromium 120.0.6099.28",
"type": "browser",
"browserName": "chromium",
"browserVersion": "120.0.6099.28",
"displayName": "chromium 120.0.6099.28"
}
],
"featureTag": {
"name": "checkout/Card payment",
"type": "feature",
"displayName": "Card payment"
},
"testSource": "JS",
"context": "mac,chrome",
"driver": "chromium",
"result": "SUCCESS",
"duration": 3499
}
Loading

0 comments on commit f54b35e

Please sign in to comment.