Repository: jhipster/jhipster-lite
Pull request #12540 (2025-05-13 10:00:05)

Front merge code coverage from Cypress and Vitest
Same as #10950, but for generated applications


Patch:
diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/application/CypressMergeCoverageApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/application/CypressMergeCoverageApplicationService.java
new file mode 100644
index 000000000..a7ce2077b
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/application/CypressMergeCoverageApplicationService.java
@@ -0,0 +1,20 @@
+package tech.jhipster.lite.generator.client.tools.cypressmergecoverage.application;
+
+import org.springframework.stereotype.Service;
+import tech.jhipster.lite.generator.client.tools.cypressmergecoverage.domain.CypressMergeCoverageModuleFactory;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@Service
+public class CypressMergeCoverageApplicationService {
+
+  private final CypressMergeCoverageModuleFactory cypressMergeCoverage;
+
+  public CypressMergeCoverageApplicationService() {
+    cypressMergeCoverage = new CypressMergeCoverageModuleFactory();
+  }
+
+  public JHipsterModule buildCypressMergeCoverage(JHipsterModuleProperties properties) {
+    return cypressMergeCoverage.buildCypressMergeCoverage(properties);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactory.java
new file mode 100644
index 000000000..783e93211
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactory.java
@@ -0,0 +1,109 @@
+package tech.jhipster.lite.generator.client.tools.cypressmergecoverage.domain;
+
+import static tech.jhipster.lite.module.domain.JHipsterModule.fileStart;
+import static tech.jhipster.lite.module.domain.JHipsterModule.from;
+import static tech.jhipster.lite.module.domain.JHipsterModule.lineAfterRegex;
+import static tech.jhipster.lite.module.domain.JHipsterModule.packageName;
+import static tech.jhipster.lite.module.domain.JHipsterModule.path;
+import static tech.jhipster.lite.module.domain.JHipsterModule.regex;
+import static tech.jhipster.lite.module.domain.JHipsterModule.scriptCommand;
+import static tech.jhipster.lite.module.domain.JHipsterModule.scriptKey;
+import static tech.jhipster.lite.module.domain.JHipsterModule.text;
+import static tech.jhipster.lite.module.domain.JHipsterModule.to;
+import static tech.jhipster.lite.module.domain.npm.JHLiteNpmVersionSource.COMMON;
+
+import java.util.function.Consumer;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.file.JHipsterSource;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+public class CypressMergeCoverageModuleFactory {
+
+  private static final JHipsterSource SOURCE = from("client/tools/cypressmergecoverage");
+  private static final String CYPRESS_COMPONENT_TESTS = "src/test/webapp/component";
+
+  public JHipsterModule buildCypressMergeCoverage(JHipsterModuleProperties properties) {
+    //@formatter:off
+    return JHipsterModule.moduleBuilder(properties)
+      .packageJson()
+        .addDependency(packageName("@cypress/code-coverage"), COMMON)
+        .addDependency(packageName("cpy-cli"), COMMON)
+        .addDependency(packageName("rimraf"), COMMON)
+        .addDependency(packageName("vite-plugin-istanbul"), COMMON)
+        .addDependency(packageName("nyc"), COMMON)
+        .addScript(
+          scriptKey("test:coverage:check"),
+          scriptCommand("npm-run-all test:coverage:clean test:coverage:copy:* test:coverage:merge test:coverage:report")
+        )
+        .addScript(scriptKey("test:coverage:clean"), scriptCommand("rimraf target/.nyc_output"))
+        .addScript(
+          scriptKey("test:coverage:copy:unit"),
+          scriptCommand(
+            "cpy --flat target/frontend-coverage/unit-tests/coverage-final.json target/.nyc_output/ --rename=unit-coverage-final.json"
+          )
+        )
+        .addScript(
+          scriptKey("test:coverage:copy:component"),
+          scriptCommand(
+            "cpy --flat target/frontend-coverage/components-tests/coverage-final.json target/.nyc_output/ --rename=component-coverage-final.json"
+          )
+        )
+        .addScript(
+          scriptKey("test:coverage:merge"),
+          scriptCommand("nyc merge target/.nyc_output target/.nyc_output/frontend-combined-coverage.json")
+        )
+        .addScript(scriptKey("test:coverage:report"), scriptCommand("nyc report --reporter=lcov --reporter=text"))
+      .and()
+      .apply(patchViteConfig())
+      .apply(patchVitestConfig())
+      .apply(patchCypressConfig(properties))
+      .files()
+        .batch(SOURCE, to("."))
+          .addTemplate(".nycrc.json")
+        .and()
+        .batch(SOURCE.append(CYPRESS_COMPONENT_TESTS), to(CYPRESS_COMPONENT_TESTS))
+          .addTemplate(".nycrc.json")
+        .and()
+        .batch(SOURCE.append(CYPRESS_COMPONENT_TESTS).append("support"), to(CYPRESS_COMPONENT_TESTS).append("/support"))
+          .addFile("component-tests.ts")
+        .and()
+      .and()
+      .build();
+    //@formatter:on
+  }
+
+  private Consumer<JHipsterModule.JHipsterModuleBuilder> patchCypressConfig(JHipsterModuleProperties properties) {
+    return moduleBuilder ->
+      moduleBuilder
+        .mandatoryReplacements()
+        .in(path(CYPRESS_COMPONENT_TESTS + "/cypress-config.ts"))
+        .add(fileStart(), "import registerCodeCoverageTasks from '@cypress/code-coverage/task';\n")
+        .add(
+          lineAfterRegex("e2e:"),
+          properties.indentation().times(2) +
+          """
+          setupNodeEvents(on, config) {
+                registerCodeCoverageTasks(on, config);
+                return config;
+              },"""
+        )
+        .add(text("supportFile: false"), "supportFile: 'src/test/webapp/component/support/component-tests.ts'");
+  }
+
+  private Consumer<JHipsterModule.JHipsterModuleBuilder> patchVitestConfig() {
+    return moduleBuilder ->
+      moduleBuilder
+        .mandatoryReplacements()
+        .in(path("vitest.config.ts"))
+        .add(regex("reportsDirectory: '(.*?)/test-results/'"), "reportsDirectory: 'target/frontend-coverage/unit-tests/'");
+  }
+
+  private Consumer<JHipsterModule.JHipsterModuleBuilder> patchViteConfig() {
+    return moduleBuilder ->
+      moduleBuilder
+        .mandatoryReplacements()
+        .in(path("vite.config.ts"))
+        .add(lineAfterRegex("from 'vite'"), "import istanbul from 'vite-plugin-istanbul';")
+        .add(text("plugins: ["), "plugins: [istanbul(), ");
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/infrastructure/primary/CypressMergeCoverageModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/infrastructure/primary/CypressMergeCoverageModuleConfiguration.java
new file mode 100644
index 000000000..738577409
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/infrastructure/primary/CypressMergeCoverageModuleConfiguration.java
@@ -0,0 +1,30 @@
+package tech.jhipster.lite.generator.client.tools.cypressmergecoverage.infrastructure.primary;
+
+import static tech.jhipster.lite.shared.slug.domain.JHLiteFeatureSlug.CODE_COVERAGE_CLIENT;
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.CYPRESS_COMPONENT_TESTS;
+import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.CYPRESS_MERGE_COVERAGE;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import tech.jhipster.lite.generator.client.tools.cypressmergecoverage.application.CypressMergeCoverageApplicationService;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization;
+import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition;
+import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource;
+
+@Configuration
+public class CypressMergeCoverageModuleConfiguration {
+
+  @Bean
+  JHipsterModuleResource cypressMergeCoverageModule(CypressMergeCoverageApplicationService cypressMergeCoverage) {
+    return JHipsterModuleResource.builder()
+      .slug(CYPRESS_MERGE_COVERAGE)
+      .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addIndentation().build())
+      .apiDoc(
+        "Frontend - Cypress merge coverage",
+        "Merge coverage from unit test vitest and component test cypress. Not working with Angular"
+      )
+      .organization(JHipsterModuleOrganization.builder().feature(CODE_COVERAGE_CLIENT).addDependency(CYPRESS_COMPONENT_TESTS).build())
+      .tags("client", "coverage", "cypress", "vitest")
+      .factory(cypressMergeCoverage::buildCypressMergeCoverage);
+  }
+}
diff --git a/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/package-info.java
new file mode 100644
index 000000000..05e4e70fe
--- /dev/null
+++ b/src/main/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/package-info.java
@@ -0,0 +1,2 @@
+@tech.jhipster.lite.BusinessContext
+package tech.jhipster.lite.generator.client.tools.cypressmergecoverage;
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java
index a58c501df..b51f9c11a 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/application/FrontendJavaBuildToolApplicationService.java
@@ -26,4 +26,8 @@ public class FrontendJavaBuildToolApplicationService {
   public JHipsterModule buildFrontendMavenCacheModule(JHipsterModuleProperties properties) {
     return frontendJavaBuildTool.buildFrontendMavenCacheModule(properties);
   }
+
+  public JHipsterModule buildMergeCypressCoverageModule(JHipsterModuleProperties properties) {
+    return frontendJavaBuildTool.buildMergeCypressCoverageModule(properties);
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
index d1c9b9ef9..19febed53 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactory.java
@@ -50,7 +50,7 @@ public class FrontendJavaBuildToolModuleFactory {
         .set(buildPropertyKey("npm.version"), buildPropertyValue(npmVersions.get("npm", JHLiteNpmVersionSource.COMMON).get()))
         .and()
       .mavenPlugins()
-        .plugin(frontendMavenPlugin())
+        .plugin(frontendMavenPlugin().build())
         .and()
       .build();
     //@formatter:on
@@ -141,7 +141,7 @@ public class FrontendJavaBuildToolModuleFactory {
       .build();
   }
 
-  private MavenPlugin frontendMavenPlugin() {
+  private MavenPlugin.MavenPluginOptionalBuilder frontendMavenPlugin() {
     return mavenPlugin()
       .groupId("com.github.eirslett")
       .artifactId("frontend-maven-plugin")
@@ -185,8 +185,7 @@ public class FrontendJavaBuildToolModuleFactory {
             <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven>
             """
           )
-      )
-      .build();
+      );
   }
 
   public JHipsterModule buildFrontendGradleModule(JHipsterModuleProperties properties) {
@@ -210,6 +209,21 @@ public class FrontendJavaBuildToolModuleFactory {
     //@formatter:on
   }
 
+  public JHipsterModule buildMergeCypressCoverageModule(JHipsterModuleProperties properties) {
+    Assert.notNull("properties", properties);
+    //@formatter:off
+    return commonModuleFiles(properties)
+      .javaBuildProperties()
+        .set(buildPropertyKey("node.version"), buildPropertyValue("v" + npmVersions.nodeVersion().get()))
+        .set(buildPropertyKey("npm.version"), buildPropertyValue(npmVersions.get("npm", JHLiteNpmVersionSource.COMMON).get()))
+      .and()
+      .mavenPlugins()
+        .plugin(mergeCypressPlugin())
+      .and()
+      .build();
+    //@formatter:on
+  }
+
   private static JHipsterModuleBuilder commonModuleFiles(JHipsterModuleProperties properties) {
     Assert.notNull(PROPERTIES_FIELD, properties);
 
@@ -274,4 +288,33 @@ public class FrontendJavaBuildToolModuleFactory {
       )
       .build();
   }
+
+  private MavenPlugin mergeCypressPlugin() {
+    return frontendMavenPlugin()
+      .addExecution(
+        pluginExecution()
+          .goals("npm")
+          .id("front component test")
+          .phase(TEST)
+          .configuration(
+            """
+            <arguments>run test:component:headless</arguments>
+            <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven>
+            """
+          )
+      )
+      .addExecution(
+        pluginExecution()
+          .goals("npm")
+          .id("front verify coverage")
+          .phase(TEST)
+          .configuration(
+            """
+            <arguments>run test:coverage:check</arguments>
+            <npmInheritsProxyConfigFromMaven>false</npmInheritsProxyConfigFromMaven>
+            """
+          )
+      )
+      .build();
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java
index db2c4c53b..e00724616 100644
--- a/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java
+++ b/src/main/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/infrastructure/primary/FrontendJavaBuildToolModuleConfiguration.java
@@ -64,4 +64,24 @@ class FrontendJavaBuildToolModuleConfiguration {
       .tags(TAGS)
       .factory(frontendJavaBuildTool::buildFrontendGradleModule);
   }
+
+  @Bean
+  JHipsterModuleResource mergeCypressMergeCoverageModule(FrontendJavaBuildToolApplicationService frontendJavaBuildTool) {
+    return JHipsterModuleResource.builder()
+      .slug(FRONTEND_MAVEN_PLUGIN_MERGE_COVERAGE)
+      .propertiesDefinition(JHipsterModulePropertiesDefinition.builder().addBasePackage().addProjectBaseName().addIndentation().build())
+      .apiDoc(FRONTEND_JAVA_PLUGIN, "Merge Cypress and vitest code coverage")
+      .organization(
+        JHipsterModuleOrganization.builder()
+          .feature(FRONTEND_JAVA_BUILD_TOOL_PLUGIN)
+          .addDependency(CYPRESS_COMPONENT_TESTS)
+          .addDependency(CLIENT_CORE)
+          .addDependency(SPRING_SERVER)
+          .addDependency(SPRING_MVC_SERVER)
+          .addDependency(MAVEN_JAVA)
+          .build()
+      )
+      .tags("cypress", "vitest", "coverage")
+      .factory(frontendJavaBuildTool::buildMergeCypressCoverageModule);
+  }
 }
diff --git a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteFeatureSlug.java b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteFeatureSlug.java
index a3f3a8070..ad88df422 100644
--- a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteFeatureSlug.java
+++ b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteFeatureSlug.java
@@ -19,6 +19,7 @@ public enum JHLiteFeatureSlug implements JHipsterFeatureSlugFactory {
   GITHUB_ACTIONS("github-actions"),
   GITLAB_CI("gitlab-ci"),
   DEPENDENCIES_UPDATES("dependencies-updates"),
+  CODE_COVERAGE_CLIENT("code-coverage-client"),
   CODE_COVERAGE_JAVA("code-coverage-java"),
   JAVA_BUILD_TOOL("java-build-tool"),
   JAVA_BUILD_TOOL_WRAPPER("java-build-tool-wrapper"),
diff --git a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
index 2f61ef8de..6cdd3bcad 100644
--- a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
+++ b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java
@@ -45,6 +45,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   FLYWAY_POSTGRESQL("flyway-postgresql", RANK_C),
   FRONTEND_MAVEN_PLUGIN("frontend-maven-plugin", RANK_S),
   FRONTEND_MAVEN_PLUGIN_CACHE("frontend-maven-plugin-cache", RANK_B),
+  FRONTEND_MAVEN_PLUGIN_MERGE_COVERAGE("frontend-maven-plugin-merge-coverage", RANK_B),
   FRONT_HEXAGONAL_ARCHITECTURE("front-hexagonal-architecture", RANK_C),
   NODE_GRADLE_PLUGIN("node-gradle-plugin", RANK_C),
   GATEWAY("gateway", RANK_C),
@@ -92,6 +93,7 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory {
   LIQUIBASE_LINTER("liquibase-linter", RANK_B),
   LOGSTASH("logstash", RANK_B),
   LOGS_SPY("logs-spy", RANK_S),
+  CYPRESS_MERGE_COVERAGE("cypress-merge-coverage", RANK_B),
   MAVEN_JAVA("maven-java", RANK_S),
   MAVEN_WRAPPER("maven-wrapper", RANK_S),
   MODERNIZER("modernizer", RANK_C),
diff --git a/src/main/resources/generator/client/tools/cypressmergecoverage/.nycrc.json.mustache b/src/main/resources/generator/client/tools/cypressmergecoverage/.nycrc.json.mustache
new file mode 100644
index 000000000..3bb010fc6
--- /dev/null
+++ b/src/main/resources/generator/client/tools/cypressmergecoverage/.nycrc.json.mustache
@@ -0,0 +1,12 @@
+{
+  "extension": [".js", ".cjs", ".mjs", ".ts", ".tsx"],
+  "reporter": ["html", "json", "lcov", "text-summary"],
+  "report-dir": "{{ projectBuildDirectory }}/frontend-coverage/combined",
+  "temp-dir": "{{ projectBuildDirectory }}/.nyc_output",
+  "check-coverage": true,
+  "branches": 70,
+  "lines": 70,
+  "functions": 70,
+  "statements": 70,
+  "exclude": ["src/test/**/*"]
+}
diff --git a/src/main/resources/generator/dependencies/common/package.json b/src/main/resources/generator/dependencies/common/package.json
index 7ba0929ef..7a329fccc 100644
--- a/src/main/resources/generator/dependencies/common/package.json
+++ b/src/main/resources/generator/dependencies/common/package.json
@@ -5,6 +5,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/cli": "7.27.2",
+    "@cypress/code-coverage": "3.14.0",
     "@eslint/js": "9.27.0",
     "@playwright/test": "1.52.0",
     "@prettier/plugin-pug": "3.4.0",
@@ -18,6 +19,7 @@
     "@vitest/coverage-istanbul": "3.1.3",
     "autoprefixer": "10.4.21",
     "browser-sync": "3.0.4",
+    "cpy-cli": "5.0.0",
     "cssnano": "7.0.7",
     "cypress": "14.3.3",
     "eslint": "9.27.0",
@@ -36,6 +38,7 @@
     "node": "22.15.1",
     "npm": "11.4.0",
     "npm-run-all2": "8.0.2",
+    "nyc": "17.1.0",
     "onchange": "7.1.0",
     "path-exists-cli": "2.0.0",
     "piqure": "2.1.1",
@@ -47,6 +50,7 @@
     "prettier-plugin-organize-imports": "4.1.0",
     "prettier-plugin-packagejson": "2.5.13",
     "recursive-copy-cli": "1.0.20",
+    "rimraf": "6.0.1",
     "start-server-and-test": "2.0.12",
     "stylelint": "16.19.1",
     "stylelint-config-concentric-order": "5.2.1",
@@ -58,6 +62,7 @@
     "typescript": "5.8.3",
     "typescript-eslint": "8.32.1",
     "vite": "6.3.5",
+    "vite-plugin-istanbul": "7.0.0",
     "vite-tsconfig-paths": "5.1.4",
     "vitest": "3.1.3",
     "vitest-sonar-reporter": "2.0.1"


Test patch:
diff --git a/src/main/resources/generator/client/svelte/vitest.config.ts.mustache b/src/main/resources/generator/client/svelte/vitest.config.ts.mustache
index a71d5af1a..e7b26a92c 100644
--- a/src/main/resources/generator/client/svelte/vitest.config.ts.mustache
+++ b/src/main/resources/generator/client/svelte/vitest.config.ts.mustache
@@ -38,7 +38,7 @@ export default defineConfig({
       clean: true,
       provider: 'istanbul',
       reportsDirectory: '{{projectBuildDirectory}}/test-results/',
-      reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
       watermarks: {
         statements: [100, 100],
         branches: [100, 100],
diff --git a/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/.nycrc.json.mustache b/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/.nycrc.json.mustache
new file mode 100644
index 000000000..cbf4d3e02
--- /dev/null
+++ b/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/.nycrc.json.mustache
@@ -0,0 +1,6 @@
+{
+  "extension": [".js", ".cjs", ".mjs", ".ts", ".tsx"],
+  "reporter": ["html", "json"],
+  "report-dir": "../../../../{{ projectBuildDirectory }}/frontend-coverage/components-tests",
+  "temp-dir": "../../../../{{ projectBuildDirectory }}/.nyc_output"
+}
diff --git a/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/support/component-tests.ts b/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/support/component-tests.ts
new file mode 100644
index 000000000..f568da8eb
--- /dev/null
+++ b/src/main/resources/generator/client/tools/cypressmergecoverage/src/test/webapp/component/support/component-tests.ts
@@ -0,0 +1,1 @@
+import '@cypress/code-coverage/support';
diff --git a/src/main/resources/generator/typescript/vitest.config.ts.mustache b/src/main/resources/generator/typescript/vitest.config.ts.mustache
index eef80168b..32d9c3b5d 100644
--- a/src/main/resources/generator/typescript/vitest.config.ts.mustache
+++ b/src/main/resources/generator/typescript/vitest.config.ts.mustache
@@ -31,7 +31,7 @@ export default defineConfig({
       exclude: [...(configDefaults.coverage.exclude as string[])],
       provider: 'istanbul',
       reportsDirectory: '{{projectBuildDirectory}}/test-results/',
-      reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
       watermarks: {
         statements: [100, 100],
         branches: [100, 100],
diff --git a/src/test/features/client/maven-cypress-merge-coverage.feature b/src/test/features/client/maven-cypress-merge-coverage.feature
new file mode 100644
index 000000000..32ed190a7
--- /dev/null
+++ b/src/test/features/client/maven-cypress-merge-coverage.feature
@@ -0,0 +1,12 @@
+Feature: Maven Cypress merge coverage module
+
+  Scenario: Should apply maven cypress merge coverage module
+    When I apply modules to default project
+      | init                    |
+      | prettier                |
+      | typescript              |
+      | react-core              |
+      | cypress-component-tests |
+      | cypress-merge-coverage  |
+    Then I should have files in ""
+      | .nycrc.json |
diff --git a/src/test/features/server/javatool/frontend-java-build-tool.feature b/src/test/features/server/javatool/frontend-java-build-tool.feature
index 9254435d0..967cbadc2 100644
--- a/src/test/features/server/javatool/frontend-java-build-tool.feature
+++ b/src/test/features/server/javatool/frontend-java-build-tool.feature
@@ -18,3 +18,8 @@ Feature: Frontend server module
       | baseName    | jhipster            |
     Then I should have files in "src/main/java/tech/jhipster/chips/wire/frontend/infrastructure/primary"
       | RedirectionResource.java |
+
+  Scenario: Should apply frontend-maven-plugin-merge-coverage module
+    When I apply "frontend-maven-plugin-merge-coverage" module to default project with maven file
+      | packageName | tech.jhipster.chips |
+    Then I should have "run test:coverage:check" in "pom.xml"
diff --git a/src/test/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.java
new file mode 100644
index 000000000..27bf23df0
--- /dev/null
+++ b/src/test/java/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.java
@@ -0,0 +1,78 @@
+package tech.jhipster.lite.generator.client.tools.cypressmergecoverage.domain;
+
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.assertThatModuleWithFiles;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.cypressTestConfigFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.eslintConfigFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.nodeDependency;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.nodeScript;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.packageJsonFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.pomFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.tsConfigFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.viteConfigFile;
+import static tech.jhipster.lite.module.infrastructure.secondary.JHipsterModulesAssertions.vitestConfigFile;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import tech.jhipster.lite.TestFileUtils;
+import tech.jhipster.lite.UnitTest;
+import tech.jhipster.lite.module.domain.JHipsterModule;
+import tech.jhipster.lite.module.domain.JHipsterModulesFixture;
+import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties;
+
+@UnitTest
+@ExtendWith(MockitoExtension.class)
+class CypressMergeCoverageModuleFactoryTest {
+
+  @InjectMocks
+  private CypressMergeCoverageModuleFactory factory;
+
+  @Test
+  void shouldBuildCypressMergeCoverageModule() {
+    JHipsterModuleProperties properties = JHipsterModulesFixture.propertiesBuilder(TestFileUtils.tmpDirForTest())
+      .projectBaseName("jhiTest")
+      .basePackage("tech.jhipster.jhlitest")
+      .build();
+
+    JHipsterModule module = factory.buildCypressMergeCoverage(properties);
+
+    assertThatModuleWithFiles(
+      module,
+      packageJsonFile(),
+      tsConfigFile(),
+      vitestConfigFile(),
+      viteConfigFile(),
+      eslintConfigFile(),
+      pomFile(),
+      cypressTestConfigFile()
+    )
+      .hasFile("package.json")
+      .containing(nodeDependency("@cypress/code-coverage"))
+      .containing(nodeDependency("cpy-cli"))
+      .containing(nodeDependency("rimraf"))
+      .containing(nodeDependency("vite-plugin-istanbul"))
+      .containing(nodeDependency("nyc"))
+      .containing(nodeScript("test:coverage:check"))
+      .containing(nodeScript("test:coverage:clean"))
+      .containing(nodeScript("test:coverage:copy:unit"))
+      .containing(nodeScript("test:coverage:copy:component"))
+      .containing(nodeScript("test:coverage:merge"))
+      .containing(nodeScript("test:coverage:report"))
+      .and()
+      .hasFile("vite.config.ts")
+      .matchingSavedSnapshot()
+      .and()
+      .hasFile("vitest.config.ts")
+      .matchingSavedSnapshot()
+      .and()
+      .hasFile(".nycrc.json")
+      .and()
+      .hasFile("src/test/webapp/component/cypress-config.ts")
+      .matchingSavedSnapshot()
+      .and()
+      .hasFile("src/test/webapp/component/.nycrc.json")
+      .and()
+      .hasFile("src/test/webapp/component/support/component-tests.ts");
+  }
+}
diff --git a/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
index a9800b3f9..22f1d2a5f 100644
--- a/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
+++ b/src/test/java/tech/jhipster/lite/generator/server/javatool/frontendmaven/domain/FrontendJavaBuildToolModuleFactoryTest.java
@@ -190,6 +190,19 @@ class FrontendJavaBuildToolModuleFactoryTest {
           """
         );
     }
+
+    @Test
+    void shouldBuildMergeCypressCoverageModule() {
+      mockNpmVersion();
+
+      JHipsterModule module = factory.buildMergeCypressCoverageModule(getProperties());
+
+      assertThatModuleWithFiles(module, pomFile())
+        .hasFile("pom.xml")
+        .containing("run test:coverage")
+        .containing("run test:component:headless")
+        .containing("run test:coverage:check");
+    }
   }
 
   @Nested
diff --git a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/JHipsterModulesAssertions.java b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/JHipsterModulesAssertions.java
index 948d8073d..6128f87cf 100644
--- a/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/JHipsterModulesAssertions.java
+++ b/src/test/java/tech/jhipster/lite/module/infrastructure/secondary/JHipsterModulesAssertions.java
@@ -82,6 +82,14 @@ public final class JHipsterModulesAssertions {
     return fileFromClasspath("generator/typescript/vitest.config.ts.mustache", "vitest.config.ts");
   }
 
+  public static ModuleFile viteConfigFile() {
+    return fileFromClasspath("projects/typescript-with-vite/vite.config.ts.mustache", "vite.config.ts");
+  }
+
+  public static ModuleFile cypressTestConfigFile() {
+    return fileFromClasspath("generator/client/common/cypress/cypress-config.ts.mustache", "src/test/webapp/component/cypress-config.ts");
+  }
+
   public static ModuleFile eslintConfigFile() {
     return fileFromClasspath("generator/typescript/eslint.config.js.mustache", "eslint.config.js");
   }
diff --git a/src/test/resources/projects/typescript-with-vite/vite.config.ts.mustache b/src/test/resources/projects/typescript-with-vite/vite.config.ts.mustache
new file mode 100644
index 000000000..8b593a00b
--- /dev/null
+++ b/src/test/resources/projects/typescript-with-vite/vite.config.ts.mustache
@@ -0,0 +1,24 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+  plugins: [react(), tsconfigPaths()],
+  build: {
+    outDir: '../../../{{projectBuildDirectory}}/classes/static',
+  },
+  root: 'src/main/webapp',
+  server: {
+    port: 9000,
+    hmr: { overlay: false },
+    proxy: {
+      '/api': {
+        target: 'http://localhost:{{serverPort}}',
+        changeOrigin: true,
+      },
+    },
+  },
+  define: {
+    'process.env': {},
+  },
+});
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModule.vitest.config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModule.vitest.config.ts.approved.txt
index 87ad4ca73..31e78aa41 100644
--- a/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModule.vitest.config.ts.approved.txt
+++ b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModule.vitest.config.ts.approved.txt
@@ -32,7 +32,7 @@ export default defineConfig({
       exclude: [...(configDefaults.coverage.exclude as string[]), 'src/main/webapp/app/injections.ts', 'src/main/webapp/app/index.tsx'],
       provider: 'istanbul',
       reportsDirectory: 'target/test-results/',
-      reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
       watermarks: {
         statements: [100, 100],
         branches: [100, 100],
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt
index 87ad4ca73..31e78aa41 100644
--- a/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt
+++ b/src/test/resources/tech/jhipster/lite/generator/client/react/core/domain/ReactModuleFactoryTest.shouldBuildModuleWithStyle.vitest.config.ts.approved.txt
@@ -32,7 +32,7 @@ export default defineConfig({
       exclude: [...(configDefaults.coverage.exclude as string[]), 'src/main/webapp/app/injections.ts', 'src/main/webapp/app/index.tsx'],
       provider: 'istanbul',
       reportsDirectory: 'target/test-results/',
-      reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
       watermarks: {
         statements: [100, 100],
         branches: [100, 100],
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.cypress-config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.cypress-config.ts.approved.txt
new file mode 100644
index 000000000..67e153583
--- /dev/null
+++ b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.cypress-config.ts.approved.txt
@@ -0,0 +1,16 @@
+import registerCodeCoverageTasks from '@cypress/code-coverage/task';
+import { defineConfig } from 'cypress';
+
+export default defineConfig({
+  e2e: {
+    setupNodeEvents(on, config) {
+      registerCodeCoverageTasks(on, config);
+      return config;
+    },
+    baseUrl: 'http://localhost:9000',
+    specPattern: 'src/test/webapp/{{ cypressTestDirectory }}/**/*.(spec|cy).ts',
+    fixturesFolder: false,
+    supportFile: 'src/test/webapp/component/support/component-tests.ts',
+    video: false,
+  },
+});
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vite.config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vite.config.ts.approved.txt
new file mode 100644
index 000000000..d8ac923a0
--- /dev/null
+++ b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vite.config.ts.approved.txt
@@ -0,0 +1,25 @@
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+import istanbul from 'vite-plugin-istanbul';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+export default defineConfig({
+  plugins: [istanbul(), react(), tsconfigPaths()],
+  build: {
+    outDir: '../../../target/classes/static',
+  },
+  root: 'src/main/webapp',
+  server: {
+    port: 9000,
+    hmr: { overlay: false },
+    proxy: {
+      '/api': {
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+      },
+    },
+  },
+  define: {
+    'process.env': {},
+  },
+});
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vitest.config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vitest.config.ts.approved.txt
new file mode 100644
index 000000000..4376d1add
--- /dev/null
+++ b/src/test/resources/tech/jhipster/lite/generator/client/tools/cypressmergecoverage/domain/CypressMergeCoverageModuleFactoryTest.shouldBuildCypressMergeCoverageModule.vitest.config.ts.approved.txt
@@ -0,0 +1,43 @@
+/// <reference types="vitest" />
+
+import tsconfigPaths from 'vite-tsconfig-paths';
+import { configDefaults, defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  plugins: [tsconfigPaths()],
+  test: {
+    reporters: ['verbose', 'vitest-sonar-reporter'],
+    outputFile: {
+      'vitest-sonar-reporter': 'target/test-results/TESTS-results-sonar.xml',
+    },
+    globals: true,
+    logHeapUsage: true,
+    poolOptions: {
+      threads: {
+        minThreads: 1,
+        maxThreads: 2,
+      },
+    },
+    environment: 'node',
+    cache: false,
+    include: ['src/test/webapp/unit/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
+    coverage: {
+      thresholds: {
+        perFile: true,
+        autoUpdate: true,
+        100: true,
+      },
+      include: ['src/main/webapp/**/*.ts?(x)'],
+      exclude: [...(configDefaults.coverage.exclude as string[])],
+      provider: 'istanbul',
+      reportsDirectory: 'target/frontend-coverage/unit-tests/',
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      watermarks: {
+        statements: [100, 100],
+        branches: [100, 100],
+        functions: [100, 100],
+        lines: [100, 100],
+      },
+    },
+  },
+});
diff --git a/src/test/resources/tech/jhipster/lite/generator/client/vue/core/domain/VueModuleFactoryTest.shouldBuildVueModule.vitest.config.ts.approved.txt b/src/test/resources/tech/jhipster/lite/generator/client/vue/core/domain/VueModuleFactoryTest.shouldBuildVueModule.vitest.config.ts.approved.txt
index bb802095c..eb2977959 100644
--- a/src/test/resources/tech/jhipster/lite/generator/client/vue/core/domain/VueModuleFactoryTest.shouldBuildVueModule.vitest.config.ts.approved.txt
+++ b/src/test/resources/tech/jhipster/lite/generator/client/vue/core/domain/VueModuleFactoryTest.shouldBuildVueModule.vitest.config.ts.approved.txt
@@ -32,7 +32,7 @@ export default defineConfig({
       exclude: [...(configDefaults.coverage.exclude as string[]), 'src/main/webapp/app/main.ts', 'src/main/webapp/app/injections.ts', 'src/main/webapp/app/router.ts', 'src/main/webapp/**/*.component.ts'],
       provider: 'istanbul',
       reportsDirectory: 'target/test-results/',
-      reporter: ['html', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
+      reporter: ['html', 'json', 'json-summary', 'text', 'text-summary', 'lcov', 'clover'],
       watermarks: {
         statements: [100, 100],
         branches: [100, 100],