From 4554f65c6e44da1c4f99216c856c66adec9a0e60 Mon Sep 17 00:00:00 2001
From: Caleb Ukle <caleb@nrwl.io>
Date: Fri, 28 Apr 2023 15:40:04 -0500
Subject: [PATCH] fix(testing): fix react CT w/ vite and dependant projects
 (#16475)

(cherry picked from commit ebcc4363d0ff44bf865c783ac3ea801b691b03d2)
---
 .../src/cypress-component-tests.test.ts       | 73 +++++++++++++++----
 .../react/plugins/component-testing/index.ts  | 43 ++++++++++-
 .../lib/add-files.ts                          |  7 ++
 scripts/depcheck/missing.ts                   |  2 +
 4 files changed, 109 insertions(+), 16 deletions(-)

diff --git a/e2e/react-extensions/src/cypress-component-tests.test.ts b/e2e/react-extensions/src/cypress-component-tests.test.ts
index 1f1ffe073683d..134889ffd9b8e 100644
--- a/e2e/react-extensions/src/cypress-component-tests.test.ts
+++ b/e2e/react-extensions/src/cypress-component-tests.test.ts
@@ -4,6 +4,7 @@ import {
   ensureCypressInstallation,
   newProject,
   runCLI,
+  runCypressTests,
   uniq,
   updateFile,
   updateJson,
@@ -146,18 +147,22 @@ export default Input;
     runCLI(
       `generate @nx/react:cypress-component-configuration --project=${appName} --generate-tests`
     );
-    expect(runCLI(`component-test ${appName} --no-watch`)).toContain(
-      'All specs passed!'
-    );
+    if (runCypressTests()) {
+      expect(runCLI(`component-test ${appName} --no-watch`)).toContain(
+        'All specs passed!'
+      );
+    }
   }, 300_000);
 
   it('should successfully component test lib being used in app', () => {
     runCLI(
       `generate @nx/react:cypress-component-configuration --project=${usedInAppLibName} --generate-tests`
     );
-    expect(runCLI(`component-test ${usedInAppLibName} --no-watch`)).toContain(
-      'All specs passed!'
-    );
+    if (runCypressTests()) {
+      expect(runCLI(`component-test ${usedInAppLibName} --no-watch`)).toContain(
+        'All specs passed!'
+      );
+    }
   }, 300_000);
 
   it('should test buildable lib not being used in app', () => {
@@ -184,9 +189,12 @@ describe(Input.name, () => {
     runCLI(
       `generate @nx/react:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build`
     );
-    expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
-      'All specs passed!'
-    );
+
+    if (runCypressTests()) {
+      expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
+        'All specs passed!'
+      );
+    }
 
     // add tailwind
     runCLI(`generate @nx/react:setup-tailwind --project=${buildableLibName}`);
@@ -213,9 +221,11 @@ ${content}`;
       }
     );
 
-    expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
-      'All specs passed!'
-    );
+    if (runCypressTests()) {
+      expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
+        'All specs passed!'
+      );
+    }
   }, 300_000);
 
   it('should work with async webpack config', () => {
@@ -250,8 +260,41 @@ ${content}`;
       return config;
     });
 
-    const results = runCLI(`component-test ${appName}`);
-    expect(results).toContain('I am from the custom async Webpack config');
-    expect(results).toContain('All specs passed!');
+    if (runCypressTests()) {
+      const results = runCLI(`component-test ${appName}`);
+      expect(results).toContain('I am from the custom async Webpack config');
+      expect(results).toContain('All specs passed!');
+    }
+  });
+
+  // flaky bc of upstream issue https://github.com/cypress-io/cypress/issues/25913
+  it.skip('should CT vite projects importing other projects', () => {
+    const viteLibName = uniq('vite-lib');
+    runCLI(
+      `generate @nrwl/react:lib ${viteLibName} --bundler=vite --no-interactive`
+    );
+
+    updateFile(`libs/${viteLibName}/src/lib/${viteLibName}.tsx`, () => {
+      return `import { Btn } from '@${projectName}/${usedInAppLibName}';
+
+export function MyComponent() {
+  return (
+    <>
+      <Btn text={'I am the app'}/>
+      <p>hello</p>
+    </>
+  );
+}
+export default MyComponent;`;
+    });
+
+    runCLI(
+      `generate @nrwl/react:cypress-component-configuration --project=${viteLibName} --generate-tests --bundler=vite --build-target=${appName}:build`
+    );
+    if (runCypressTests()) {
+      expect(runCLI(`component-test ${viteLibName}`)).toContain(
+        'All specs passed!'
+      );
+    }
   });
 });
diff --git a/packages/react/plugins/component-testing/index.ts b/packages/react/plugins/component-testing/index.ts
index 300f1a17eb33e..0c55692280dab 100644
--- a/packages/react/plugins/component-testing/index.ts
+++ b/packages/react/plugins/component-testing/index.ts
@@ -5,6 +5,7 @@ import {
 import type { CypressExecutorOptions } from '@nx/cypress/src/executors/cypress/cypress.impl';
 import {
   ExecutorContext,
+  joinPathFragments,
   logger,
   parseTargetString,
   ProjectGraph,
@@ -19,7 +20,8 @@ import {
   getProjectConfigByPath,
 } from '@nx/cypress/src/utils/ct-helpers';
 
-import type { Configuration } from 'webpack';
+import { existsSync, lstatSync } from 'fs';
+import { dirname, join } from 'path';
 type ViteDevServer = {
   framework: 'react';
   bundler: 'vite';
@@ -66,6 +68,36 @@ export function nxComponentTestingPreset(
       specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
       devServer: {
         ...({ framework: 'react', bundler: 'vite' } as const),
+        viteConfig: async () => {
+          const normalizedPath = ['.ts', '.js'].some((ext) =>
+            pathToConfig.endsWith(ext)
+          )
+            ? pathToConfig
+            : dirname(pathToConfig);
+          const viteConfigPath = findViteConfig(normalizedPath);
+
+          const { mergeConfig, loadConfigFromFile, searchForWorkspaceRoot } =
+            (await import('vite')) as typeof import('vite');
+
+          const resolved = await loadConfigFromFile(
+            {
+              mode: 'watch',
+              command: 'serve',
+            },
+            viteConfigPath
+          );
+          return mergeConfig(resolved.config, {
+            server: {
+              fs: {
+                allow: [
+                  searchForWorkspaceRoot(normalizedPath),
+                  workspaceRoot,
+                  joinPathFragments(workspaceRoot, 'node_modules/vite'),
+                ],
+              },
+            },
+          });
+        },
       },
     };
   }
@@ -232,3 +264,12 @@ function buildTargetWebpack(
     };
   }
 }
+function findViteConfig(projectRootFullPath: string): string {
+  const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
+
+  for (const ext of allowsExt) {
+    if (existsSync(join(projectRootFullPath, `vite.config.${ext}`))) {
+      return join(projectRootFullPath, `vite.config.${ext}`);
+    }
+  }
+}
diff --git a/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts b/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts
index 0b90fd8b61d09..aae5fb9125c79 100644
--- a/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts
+++ b/packages/react/src/generators/cypress-component-configuration/lib/add-files.ts
@@ -56,6 +56,13 @@ export async function addFiles(
     addDependenciesToPackageJson(tree, {}, { '@nx/webpack': nxVersion });
   }
 
+  if (
+    options.bundler === 'vite' ||
+    (!options.bundler && actualBundler === 'vite')
+  ) {
+    addDependenciesToPackageJson(tree, {}, { '@nx/vite': nxVersion });
+  }
+
   if (options.generateTests) {
     const filePaths = [];
     visitNotIgnoredFiles(tree, projectConfig.sourceRoot, (filePath) => {
diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts
index 5382ecc6c8090..e742883d3c018 100644
--- a/scripts/depcheck/missing.ts
+++ b/scripts/depcheck/missing.ts
@@ -114,6 +114,8 @@ const IGNORE_MATCHES_IN_PACKAGE = {
     'url-loader',
     'webpack',
     'webpack-merge',
+    // used via the CT react plugin installed via vite plugin
+    'vite',
   ],
   'react-native': ['@nx/storybook'],
   rollup: ['@swc/core'],