From 9dfc66cc99a2ccc55fb424e87621148c28543aeb Mon Sep 17 00:00:00 2001 From: Caleb Ukle Date: Fri, 14 Apr 2023 10:35:22 -0500 Subject: [PATCH] fix(testing): support custom workspaceRoot for angular CT (#15485) (cherry picked from commit 26fbd1daa05cd94dbba88a68186eb2f24357ba90) --- .../src/cypress-component-tests.test.ts | 34 +++ packages/angular/plugins/component-testing.ts | 201 ++++++++++++------ packages/cypress/src/utils/ct-helpers.ts | 2 +- scripts/depcheck/missing.ts | 2 + 4 files changed, 172 insertions(+), 67 deletions(-) diff --git a/e2e/angular-extensions/src/cypress-component-tests.test.ts b/e2e/angular-extensions/src/cypress-component-tests.test.ts index 3d52b7ed70e08..6f62a301a322a 100644 --- a/e2e/angular-extensions/src/cypress-component-tests.test.ts +++ b/e2e/angular-extensions/src/cypress-component-tests.test.ts @@ -9,8 +9,10 @@ import { updateFile, updateProjectConfig, removeFile, + checkFilesExist, } from '../../utils'; import { names } from '@nrwl/devkit'; +import { join } from 'path'; describe('Angular Cypress Component Tests', () => { let projectName: string; @@ -114,6 +116,20 @@ describe('Angular Cypress Component Tests', () => { ); } }); + + it('should use root level tailwinds config', () => { + useRootLevelTailwindConfig( + join('libs', buildableLibName, 'tailwind.config.js') + ); + checkFilesExist('tailwind.config.js'); + checkFilesDoNotExist(`libs/${buildableLibName}/tailwind.config.js`); + + if (runCypressTests()) { + expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain( + 'All specs passed!' + ); + } + }); }); function createApp(appName: string) { @@ -386,3 +402,21 @@ function updateBuilableLibTestsToAssertAppStyles( } ); } + +function useRootLevelTailwindConfig(existingConfigPath: string) { + createFile( + 'tailwind.config.js', + `const { join } = require('path'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [join(__dirname, '**/*.{html,js,ts}')], + theme: { + extend: {}, + }, + plugins: [], +}; +` + ); + removeFile(existingConfigPath); +} diff --git a/packages/angular/plugins/component-testing.ts b/packages/angular/plugins/component-testing.ts index 6410b4c2fea68..a3e6fe4022e64 100644 --- a/packages/angular/plugins/component-testing.ts +++ b/packages/angular/plugins/component-testing.ts @@ -19,11 +19,11 @@ import { readCachedProjectGraph, readTargetOptions, stripIndents, - workspaceRoot, } from '@nrwl/devkit'; import { existsSync, lstatSync, mkdirSync, writeFileSync } from 'fs'; import { dirname, join, relative, sep } from 'path'; import type { BrowserBuilderSchema } from '../src/builders/webpack-browser/schema'; +import { gte } from 'semver'; /** * Angular nx preset for Cypress Component Testing @@ -78,11 +78,14 @@ ${e.stack ? e.stack : e}` Has project config? ${!!graph.nodes?.[buildTarget.project]?.data}`); } - const fromWorkspaceRoot = relative(workspaceRoot, pathToConfig); + const fromWorkspaceRoot = relative(ctContext.root, pathToConfig); const normalizedFromWorkspaceRootPath = lstatSync(pathToConfig).isFile() ? dirname(fromWorkspaceRoot) : fromWorkspaceRoot; - const offset = offsetFromRoot(normalizedFromWorkspaceRootPath); + const offset = isOffsetNeeded(ctContext, ctProjectConfig) + ? offsetFromRoot(normalizedFromWorkspaceRootPath) + : undefined; + const buildContext = createExecutorContext( graph, graph.nodes[buildTarget.project]?.data.targets, @@ -101,6 +104,15 @@ ${e.stack ? e.stack : e}` ...nxBaseCypressPreset(pathToConfig), // NOTE: cannot use a glob pattern since it will break cypress generated tsconfig. specPattern: ['src/**/*.cy.ts', 'src/**/*.cy.js'], + // cypress defaults to a relative path from the workspaceRoot instead of projectRoot + // set as absolute path in case this changes internally to cypress, this path isn't OS dependent + indexHtmlFile: joinPathFragments( + ctContext.root, + ctProjectConfig.root, + 'cypress', + 'support', + 'component-index.html' + ), devServer: { // cypress uses string union type, // need to use const to prevent typing to string @@ -156,8 +168,12 @@ function getBuildableTarget(ctContext: ExecutorContext) { function normalizeBuildTargetOptions( buildContext: ExecutorContext, ctContext: ExecutorContext, - offset: string -): { root: string; sourceRoot: string; buildOptions: BrowserBuilderSchema } { + offset?: string +): { + root: string; + sourceRoot: string; + buildOptions: BrowserBuilderSchema & { workspaceRoot: string }; +} { const options = readTargetOptions( { project: buildContext.projectName, @@ -168,39 +184,40 @@ function normalizeBuildTargetOptions( ); const buildOptions = withSchemaDefaults(options); - // polyfill entries might be local files or files that are resolved from node_modules - // like zone.js. - // prevents error from webpack saying can't find /zone.js. - const handlePolyfillPath = (polyfill: string) => { - const maybeFullPath = join(workspaceRoot, polyfill.split('/').join(sep)); - if (existsSync(maybeFullPath)) { - return joinPathFragments(offset, polyfill); - } - return polyfill; - }; - // paths need to be unix paths for angular devkit - buildOptions.polyfills = - Array.isArray(buildOptions.polyfills) && buildOptions.polyfills.length > 0 - ? (buildOptions.polyfills as string[]).map((p) => handlePolyfillPath(p)) - : handlePolyfillPath(buildOptions.polyfills as string); - - buildOptions.main = joinPathFragments(offset, buildOptions.main); - buildOptions.index = - typeof buildOptions.index === 'string' - ? joinPathFragments(offset, buildOptions.index) - : { - ...buildOptions.index, - input: joinPathFragments(offset, buildOptions.index.input), - }; // cypress creates a tsconfig if one isn't preset // that contains all the support required for angular and component tests delete buildOptions.tsConfig; - buildOptions.fileReplacements = buildOptions.fileReplacements.map((fr) => { - fr.replace = joinPathFragments(offset, fr.replace); - fr.with = joinPathFragments(offset, fr.with); - return fr; - }); + if (offset) { + // polyfill entries might be local files or files that are resolved from node_modules + // like zone.js. + // prevents error from webpack saying can't find /zone.js. + const handlePolyfillPath = (polyfill: string) => { + const maybeFullPath = join(ctContext.root, polyfill.split('/').join(sep)); + if (existsSync(maybeFullPath)) { + return joinPathFragments(offset, polyfill); + } + return polyfill; + }; + // paths need to be unix paths for angular devkit + buildOptions.polyfills = + Array.isArray(buildOptions.polyfills) && buildOptions.polyfills.length > 0 + ? (buildOptions.polyfills as string[]).map((p) => handlePolyfillPath(p)) + : handlePolyfillPath(buildOptions.polyfills as string); + buildOptions.main = joinPathFragments(offset, buildOptions.main); + buildOptions.index = + typeof buildOptions.index === 'string' + ? joinPathFragments(offset, buildOptions.index) + : { + ...buildOptions.index, + input: joinPathFragments(offset, buildOptions.index.input), + }; + buildOptions.fileReplacements = buildOptions.fileReplacements.map((fr) => { + fr.replace = joinPathFragments(offset, fr.replace); + fr.with = joinPathFragments(offset, fr.with); + return fr; + }); + } // if the ct project isn't being used in the build project // then we don't want to have the assets/scripts/styles be included to @@ -213,29 +230,31 @@ function normalizeBuildTargetOptions( ctContext.projectName ) ) { - buildOptions.assets = buildOptions.assets.map((asset) => { - return typeof asset === 'string' - ? joinPathFragments(offset, asset) - : { ...asset, input: joinPathFragments(offset, asset.input) }; - }); - buildOptions.styles = buildOptions.styles.map((style) => { - return typeof style === 'string' - ? joinPathFragments(offset, style) - : { ...style, input: joinPathFragments(offset, style.input) }; - }); - buildOptions.scripts = buildOptions.scripts.map((script) => { - return typeof script === 'string' - ? joinPathFragments(offset, script) - : { ...script, input: joinPathFragments(offset, script.input) }; - }); - if (buildOptions.stylePreprocessorOptions?.includePaths.length > 0) { - buildOptions.stylePreprocessorOptions = { - includePaths: buildOptions.stylePreprocessorOptions.includePaths.map( - (path) => { - return joinPathFragments(offset, path); - } - ), - }; + if (offset) { + buildOptions.assets = buildOptions.assets.map((asset) => { + return typeof asset === 'string' + ? joinPathFragments(offset, asset) + : { ...asset, input: joinPathFragments(offset, asset.input) }; + }); + buildOptions.styles = buildOptions.styles.map((style) => { + return typeof style === 'string' + ? joinPathFragments(offset, style) + : { ...style, input: joinPathFragments(offset, style.input) }; + }); + buildOptions.scripts = buildOptions.scripts.map((script) => { + return typeof script === 'string' + ? joinPathFragments(offset, script) + : { ...script, input: joinPathFragments(offset, script.input) }; + }); + if (buildOptions.stylePreprocessorOptions?.includePaths.length > 0) { + buildOptions.stylePreprocessorOptions = { + includePaths: buildOptions.stylePreprocessorOptions.includePaths.map( + (path) => { + return joinPathFragments(offset, path); + } + ), + }; + } } } else { const stylePath = getTempStylesForTailwind(ctContext); @@ -256,9 +275,15 @@ Note: this may fail, setting the correct 'sourceRoot' for ${buildContext.project } return { - root: joinPathFragments(offset, config.root), - sourceRoot: joinPathFragments(offset, config.sourceRoot), - buildOptions, + root: offset ? joinPathFragments(offset, config.root) : config.root, + sourceRoot: offset + ? joinPathFragments(offset, config.sourceRoot) + : config.sourceRoot, + buildOptions: { + ...buildOptions, + // this property is only valid for cy v12.9.0+ + workspaceRoot: offset ? undefined : ctContext.root, + }, }; } @@ -309,13 +334,14 @@ function getTempStylesForTailwind(ctExecutorContext: ExecutorContext) { ctProjectConfig.root, 'tailwind.config' ); - const isTailWindInCtProject = - existsSync(ctProjectTailwindConfig + '.js') || - existsSync(ctProjectTailwindConfig + '.cjs'); + const exts = ['js', 'cjs']; + const isTailWindInCtProject = exts.some((ext) => + existsSync(`${ctProjectTailwindConfig}.${ext}`) + ); const rootTailwindPath = join(ctExecutorContext.root, 'tailwind.config'); - const isTailWindInRoot = - existsSync(rootTailwindPath + '.js') || - existsSync(rootTailwindPath + '.cjs'); + const isTailWindInRoot = exts.some((ext) => + existsSync(`${rootTailwindPath}.${ext}`) + ); if (isTailWindInRoot || isTailWindInCtProject) { const pathToStyle = getTempTailwindPath(ctExecutorContext); @@ -339,3 +365,46 @@ function getTempStylesForTailwind(ctExecutorContext: ExecutorContext) { } } } + +function isOffsetNeeded( + ctExecutorContext: ExecutorContext, + ctProjectConfig: ProjectConfiguration +) { + try { + const { version = null } = require('cypress/package.json'); + + const supportsWorkspaceRoot = !!version && gte(version, '12.9.0'); + + // if using cypress + existsSync( + join( + ctExecutorContext.root, + ctProjectConfig.root, + `tailwind.config.${ext}` + ) + ) + ) + ) { + return true; + } + + return false; + } catch (e) { + if (process.env.NX_VERBOSE_LOGGING === 'true') { + logger.error(e); + } + // unable to determine if we don't require an offset + // safest to assume we do + return true; + } +} diff --git a/packages/cypress/src/utils/ct-helpers.ts b/packages/cypress/src/utils/ct-helpers.ts index e0c85d16f8115..667e6b2fbe87e 100644 --- a/packages/cypress/src/utils/ct-helpers.ts +++ b/packages/cypress/src/utils/ct-helpers.ts @@ -39,7 +39,7 @@ export function getTempTailwindPath(context: ExecutorContext) { } /** - * Checks if the childProjectName is a decendent of the parentProjectName + * Checks if the childProjectName is a descendent of the parentProjectName * in the project graph **/ export function isCtProjectUsingBuildProject( diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index 174c6adbbf417..c850216098fbc 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -52,6 +52,8 @@ const IGNORE_MATCHES_IN_PACKAGE = { 'sass', 'stylus', 'tailwindcss', + // used in the CT angular plugin where Cy is already installed to use it. + 'cypress', ], cli: ['nx'], cypress: ['cypress', '@angular-devkit/schematics', '@nrwl/cypress', 'vite'],