diff --git a/e2e/angular-extensions/src/cypress-component-tests.test.ts b/e2e/angular-extensions/src/cypress-component-tests.test.ts
index 8b025003f60cc..3d52b7ed70e08 100644
--- a/e2e/angular-extensions/src/cypress-component-tests.test.ts
+++ b/e2e/angular-extensions/src/cypress-component-tests.test.ts
@@ -8,6 +8,7 @@ import {
uniq,
updateFile,
updateProjectConfig,
+ removeFile,
} from '../../utils';
import { names } from '@nrwl/devkit';
@@ -19,20 +20,120 @@ describe('Angular Cypress Component Tests', () => {
beforeAll(async () => {
projectName = newProject({ name: uniq('cy-ng') });
- runCLI(`generate @nrwl/angular:app ${appName} --no-interactive`);
+
+ createApp(appName);
+
+ createLib(projectName, appName, usedInAppLibName);
+ useLibInApp(projectName, appName, usedInAppLibName);
+
+ createBuildableLib(projectName, buildableLibName);
+
+ useWorkspaceAssetsInApp(appName);
+ });
+
+ afterAll(() => cleanupProject());
+
+ it('should test app', () => {
runCLI(
- `generate @nrwl/angular:component fancy-component --project=${appName} --no-interactive`
+ `generate @nrwl/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive`
);
- runCLI(`generate @nrwl/angular:lib ${usedInAppLibName} --no-interactive`);
+ 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 @nrwl/angular:component btn --project=${usedInAppLibName} --inlineTemplate --inlineStyle --export --no-interactive`
+ `generate @nrwl/angular:cypress-component-configuration --project=${usedInAppLibName} --generate-tests --no-interactive`
);
+ 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', () => {
+ expect(() => {
+ // should error since no edge in graph between lib and app
+ runCLI(
+ `generate @nrwl/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --no-interactive`
+ );
+ }).toThrow();
+
+ updateTestToAssertTailwindIsNotApplied(buildableLibName);
+
+ runCLI(
+ `generate @nrwl/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build --no-interactive`
+ );
+ if (runCypressTests()) {
+ expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
+ 'All specs passed!'
+ );
+ }
+ // add tailwind
runCLI(
- `generate @nrwl/angular:component btn-standalone --project=${usedInAppLibName} --inlineTemplate --inlineStyle --export --standalone --no-interactive`
+ `generate @nrwl/angular:setup-tailwind --project=${buildableLibName}`
+ );
+ updateFile(
+ `libs/${buildableLibName}/src/lib/input/input.component.cy.ts`,
+ (content) => {
+ // text-green-500 should now apply
+ return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)');
+ }
);
updateFile(
- `libs/${usedInAppLibName}/src/lib/btn/btn.component.ts`,
- `
+ `libs/${buildableLibName}/src/lib/input-standalone/input-standalone.component.cy.ts`,
+ (content) => {
+ // text-green-500 should now apply
+ return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)');
+ }
+ );
+
+ if (runCypressTests()) {
+ expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
+ 'All specs passed!'
+ );
+ checkFilesDoNotExist(`tmp/libs/${buildableLibName}/ct-styles.css`);
+ }
+ }, 300_000);
+
+ it('should test lib with implicit dep on buildTarget', () => {
+ // creates graph like buildableLib -> lib -> app
+ // updates the apps styles and they should apply to the buildableLib
+ // even though app is not directly connected to buildableLib
+ useBuildableLibInLib(projectName, buildableLibName, usedInAppLibName);
+
+ updateBuilableLibTestsToAssertAppStyles(appName, buildableLibName);
+
+ if (runCypressTests()) {
+ expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
+ 'All specs passed!'
+ );
+ }
+ });
+});
+
+function createApp(appName: string) {
+ runCLI(`generate @nrwl/angular:app ${appName} --no-interactive`);
+ runCLI(
+ `generate @nrwl/angular:component fancy-component --project=${appName} --no-interactive`
+ );
+}
+
+function createLib(projectName: string, appName: string, libName: string) {
+ runCLI(`generate @nrwl/angular:lib ${libName} --no-interactive`);
+ runCLI(
+ `generate @nrwl/angular:component btn --project=${libName} --inlineTemplate --inlineStyle --export --no-interactive`
+ );
+ runCLI(
+ `generate @nrwl/angular:component btn-standalone --project=${libName} --inlineTemplate --inlineStyle --export --standalone --no-interactive`
+ );
+ updateFile(
+ `libs/${libName}/src/lib/btn/btn.component.ts`,
+ `
import { Component, Input } from '@angular/core';
@Component({
@@ -44,10 +145,10 @@ export class BtnComponent {
@Input() text = 'something';
}
`
- );
- updateFile(
- `libs/${usedInAppLibName}/src/lib/btn-standalone/btn-standalone.component.ts`,
- `
+ );
+ updateFile(
+ `libs/${libName}/src/lib/btn-standalone/btn-standalone.component.ts`,
+ `
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
@@ -61,58 +162,24 @@ export class BtnStandaloneComponent {
@Input() text = 'something';
}
`
- );
- // use lib in the app
- createFile(
- `apps/${appName}/src/app/app.component.html`,
- `
-<${projectName}-btn>${projectName}-btn>
-<${projectName}-btn-standalone>${projectName}-btn-standalone>
-<${projectName}-nx-welcome>${projectName}-nx-welcome>
-`
- );
- const btnModuleName = names(usedInAppLibName).className;
- updateFile(
- `apps/${appName}/src/app/app.component.scss`,
- `
-@use 'styleguide' as *;
-
-h1 {
- @include headline;
-}`
- );
- updateFile(
- `apps/${appName}/src/app/app.module.ts`,
- `
-import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
-import {${btnModuleName}Module} from "@${projectName}/${usedInAppLibName}";
-
-import { AppComponent } from './app.component';
-import { NxWelcomeComponent } from './nx-welcome.component';
-
-@NgModule({
- declarations: [AppComponent, NxWelcomeComponent],
- imports: [BrowserModule, ${btnModuleName}Module],
- providers: [],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
-`
- );
+ );
+}
- runCLI(
- `generate @nrwl/angular:lib ${buildableLibName} --buildable --no-interactive`
- );
- runCLI(
- `generate @nrwl/angular:component input --project=${buildableLibName} --inlineTemplate --inlineStyle --export --no-interactive`
- );
- runCLI(
- `generate @nrwl/angular:component input-standalone --project=${buildableLibName} --inlineTemplate --inlineStyle --export --standalone --no-interactive`
- );
- updateFile(
- `libs/${buildableLibName}/src/lib/input/input.component.ts`,
- `
+function createBuildableLib(projectName: string, libName: string) {
+ // create lib
+ runCLI(`generate @nrwl/angular:lib ${libName} --buildable --no-interactive`);
+ // create cmp for lib
+ runCLI(
+ `generate @nrwl/angular:component input --project=${libName} --inlineTemplate --inlineStyle --export --no-interactive`
+ );
+ // create standlone cmp for lib
+ runCLI(
+ `generate @nrwl/angular:component input-standalone --project=${libName} --inlineTemplate --inlineStyle --export --standalone --no-interactive`
+ );
+ // update cmp implmentation to use tailwind clasasserting in tests
+ updateFile(
+ `libs/${libName}/src/lib/input/input.component.ts`,
+ `
import {Component, Input} from '@angular/core';
@Component({
@@ -124,10 +191,10 @@ import {Component, Input} from '@angular/core';
@Input() readOnly = false;
}
`
- );
- updateFile(
- `libs/${buildableLibName}/src/lib/input-standalone/input-standalone.component.ts`,
- `
+ );
+ updateFile(
+ `libs/${libName}/src/lib/input-standalone/input-standalone.component.ts`,
+ `
import {Component, Input} from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
@@ -141,13 +208,55 @@ import {CommonModule} from '@angular/common';
@Input() readOnly = false;
}
`
- );
+ );
+}
- // make sure assets from the workspace root work.
- createFile('libs/assets/data.json', JSON.stringify({ data: 'data' }));
- createFile(
- 'assets/styles/styleguide.scss',
- `
+function useLibInApp(projectName: string, appName: string, libName: string) {
+ createFile(
+ `apps/${appName}/src/app/app.component.html`,
+ `
+<${projectName}-btn>${projectName}-btn>
+<${projectName}-btn-standalone>${projectName}-btn-standalone>
+<${projectName}-nx-welcome>${projectName}-nx-welcome>
+`
+ );
+ const btnModuleName = names(libName).className;
+ updateFile(
+ `apps/${appName}/src/app/app.component.scss`,
+ `
+@use 'styleguide' as *;
+
+h1 {
+ @include headline;
+}`
+ );
+ updateFile(
+ `apps/${appName}/src/app/app.module.ts`,
+ `
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import {${btnModuleName}Module} from "@${projectName}/${libName}";
+
+import { AppComponent } from './app.component';
+import { NxWelcomeComponent } from './nx-welcome.component';
+
+@NgModule({
+ declarations: [AppComponent, NxWelcomeComponent],
+ imports: [BrowserModule, ${btnModuleName}Module],
+ providers: [],
+ bootstrap: [AppComponent],
+})
+export class AppModule {}
+`
+ );
+}
+
+function useWorkspaceAssetsInApp(appName: string) {
+ // make sure assets from the workspace root work.
+ createFile('libs/assets/data.json', JSON.stringify({ data: 'data' }));
+ createFile(
+ 'assets/styles/styleguide.scss',
+ `
@mixin headline {
font-weight: bold;
color: darkkhaki;
@@ -155,54 +264,24 @@ import {CommonModule} from '@angular/common';
font-weight: 24px;
}
`
- );
- updateProjectConfig(appName, (config) => {
- config.targets['build'].options.stylePreprocessorOptions = {
- includePaths: ['assets/styles'],
- };
- config.targets['build'].options.assets.push({
- glob: '**/*',
- input: 'libs/assets',
- output: 'assets',
- });
- return config;
+ );
+ updateProjectConfig(appName, (config) => {
+ config.targets['build'].options.stylePreprocessorOptions = {
+ includePaths: ['assets/styles'],
+ };
+ config.targets['build'].options.assets.push({
+ glob: '**/*',
+ input: 'libs/assets',
+ output: 'assets',
});
+ return config;
});
+}
- afterAll(() => cleanupProject());
-
- it('should test app', () => {
- runCLI(
- `generate @nrwl/angular:cypress-component-configuration --project=${appName} --generate-tests --no-interactive`
- );
- 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 @nrwl/angular:cypress-component-configuration --project=${usedInAppLibName} --generate-tests --no-interactive`
- );
- 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', () => {
- expect(() => {
- // should error since no edge in graph between lib and app
- runCLI(
- `generate @nrwl/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --no-interactive`
- );
- }).toThrow();
- createFile(
- `libs/${buildableLibName}/src/lib/input/input.component.cy.ts`,
- `
+function updateTestToAssertTailwindIsNotApplied(libName: string) {
+ createFile(
+ `libs/${libName}/src/lib/input/input.component.cy.ts`,
+ `
import { MountConfig } from 'cypress/angular';
import { InputComponent } from './input.component';
@@ -229,11 +308,11 @@ describe(InputComponent.name, () => {
});
});
`
- );
+ );
- createFile(
- `libs/${buildableLibName}/src/lib/input-standalone/input-standalone.component.cy.ts`,
- `
+ createFile(
+ `libs/${libName}/src/lib/input-standalone/input-standalone.component.cy.ts`,
+ `
import { MountConfig } from 'cypress/angular';
import { InputStandaloneComponent } from './input-standalone.component';
@@ -260,39 +339,50 @@ describe(InputStandaloneComponent.name, () => {
});
});
`
- );
-
- runCLI(
- `generate @nrwl/angular:cypress-component-configuration --project=${buildableLibName} --generate-tests --build-target=${appName}:build --no-interactive`
- );
- if (runCypressTests()) {
- expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
- 'All specs passed!'
- );
- }
+ );
+}
+function useBuildableLibInLib(
+ projectName: string,
+ buildableLibName: string,
+ libName: string
+) {
+ const buildLibNames = names(buildableLibName);
+ // use the buildable lib in lib so now buildableLib has an indirect dep on app
+ updateFile(
+ `libs/${libName}/src/lib/btn-standalone/btn-standalone.component.ts`,
+ `
+import { Component, Input } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { InputStandaloneComponent } from '@${projectName}/${buildLibNames.fileName}';
+@Component({
+ selector: '${projectName}-btn-standalone',
+ standalone: true,
+ imports: [CommonModule, InputStandaloneComponent],
+ template: '${projectName} <${projectName}-input-standalone>${projectName}-input-standalone>',
+ styles: [],
+})
+export class BtnStandaloneComponent {
+ @Input() text = 'something';
+}
+`
+ );
+}
- // add tailwind
- runCLI(
- `generate @nrwl/angular:setup-tailwind --project=${buildableLibName}`
- );
- updateFile(
- `libs/${buildableLibName}/src/lib/input/input.component.cy.ts`,
- (content) => {
- // text-green-500 should now apply
- return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)');
- }
- );
- updateFile(
- `libs/${buildableLibName}/src/lib/input-standalone/input-standalone.component.cy.ts`,
- (content) => {
- // text-green-500 should now apply
- return content.replace('rgb(0, 0, 0)', 'rgb(34, 197, 94)');
- }
- );
+function updateBuilableLibTestsToAssertAppStyles(
+ appName: string,
+ buildableLibName: string
+) {
+ updateFile(
+ `apps/${appName}/src/styles.css`,
+ `label {color: pink !important;}`
+ );
- expect(runCLI(`component-test ${buildableLibName} --no-watch`)).toContain(
- 'All specs passed!'
- );
- checkFilesDoNotExist(`tmp/libs/${buildableLibName}/ct-styles.css`);
- }, 300_000);
-});
+ removeFile(`libs/${buildableLibName}/src/lib/input/input.component.cy.ts`);
+ updateFile(
+ `libs/${buildableLibName}/src/lib/input-standalone/input-standalone.component.cy.ts`,
+ (content) => {
+ // app styles should now apply
+ return content.replace('rgb(34, 197, 94)', 'rgb(255, 192, 203)');
+ }
+ );
+}
diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts
index 53c7028962836..4ed9976e7cc7f 100644
--- a/e2e/utils/index.ts
+++ b/e2e/utils/index.ts
@@ -535,6 +535,10 @@ export function runCypressTests() {
if (process.env.NX_E2E_RUN_CYPRESS === 'true') {
ensureCypressInstallation();
return true;
+ } else {
+ console.warn(
+ 'Not running Cypress because NX_E2E_RUN_CYPRESS is not set to true.'
+ );
}
return false;
}
diff --git a/packages/angular/plugins/component-testing.ts b/packages/angular/plugins/component-testing.ts
index 9554cbe679ae7..56f3c63a78511 100644
--- a/packages/angular/plugins/component-testing.ts
+++ b/packages/angular/plugins/component-testing.ts
@@ -22,7 +22,7 @@ import {
workspaceRoot,
} from '@nrwl/devkit';
import { existsSync, lstatSync, mkdirSync, writeFileSync } from 'fs';
-import { dirname, join, relative } from 'path';
+import { dirname, join, relative, sep } from 'path';
import type { BrowserBuilderSchema } from '../src/builders/webpack-browser/webpack-browser.impl';
/**
@@ -168,13 +168,22 @@ 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) =>
- joinPathFragments(offset, p)
- )
- : joinPathFragments(offset, buildOptions.polyfills as string);
+ ? (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'
@@ -197,6 +206,7 @@ function normalizeBuildTargetOptions(
// then we don't want to have the assets/scripts/styles be included to
// prevent inclusion of unintended stuff like tailwind
if (
+ buildContext.projectName === ctContext.projectName ||
isCtProjectUsingBuildProject(
ctContext.projectGraph,
buildContext.projectName,
diff --git a/packages/cypress/src/utils/ct-helpers.ts b/packages/cypress/src/utils/ct-helpers.ts
index ec2c0eef326df..e0c85d16f8115 100644
--- a/packages/cypress/src/utils/ct-helpers.ts
+++ b/packages/cypress/src/utils/ct-helpers.ts
@@ -39,20 +39,36 @@ export function getTempTailwindPath(context: ExecutorContext) {
}
/**
- * also returns true if the ct project and build project are the same.
- * i.e. component testing inside an app.
- */
+ * Checks if the childProjectName is a decendent of the parentProjectName
+ * in the project graph
+ **/
export function isCtProjectUsingBuildProject(
graph: ProjectGraph,
parentProjectName: string,
childProjectName: string
-) {
- return (
- parentProjectName === childProjectName ||
- graph.dependencies[parentProjectName].some(
- (p) => p.target === childProjectName
- )
+): boolean {
+ const isProjectDirectDep = graph.dependencies[parentProjectName].some(
+ (p) => p.target === childProjectName
);
+ if (isProjectDirectDep) {
+ return true;
+ }
+ const maybeIntermediateProjects = graph.dependencies[
+ parentProjectName
+ ].filter((p) => !graph.externalNodes[p.target]);
+
+ for (const maybeIntermediateProject of maybeIntermediateProjects) {
+ if (
+ isCtProjectUsingBuildProject(
+ graph,
+ maybeIntermediateProject.target,
+ childProjectName
+ )
+ ) {
+ return true;
+ }
+ }
+ return false;
}
export function getProjectConfigByPath(