diff --git a/tools/generators/cypress-component-configuration/README.md b/tools/generators/cypress-component-configuration/README.md new file mode 100644 index 00000000000000..ebcbdfdbc4b6eb --- /dev/null +++ b/tools/generators/cypress-component-configuration/README.md @@ -0,0 +1,38 @@ +# cypress-component-configuration + +Workspace Generator for setting up Cypress component testing for a project + + + +- [Usage](#usage) + - [Examples](#examples) +- [Options](#options) + - [`project`](#project) + + + +## Usage + +```sh +yarn nx workspace-generator cypress-component-configuration ... +``` + +Show what will be generated without writing to disk: + +```sh +yarn nx workspace-generator cypress-component-configuration --dry-run +``` + +### Examples + +```sh +yarn nx workspace-generator cypress-component-configuration +``` + +## Options + +#### `project` + +Type: `string` + +The name of the project to add cypress component testing to diff --git a/tools/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ b/tools/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ new file mode 100644 index 00000000000000..ca52cf041bbf2c --- /dev/null +++ b/tools/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ @@ -0,0 +1,3 @@ +import { baseConfig } from '@fluentui/scripts-cypress'; + +export default baseConfig; diff --git a/tools/generators/cypress-component-configuration/files/tsconfig.cy.json__tmpl__ b/tools/generators/cypress-component-configuration/files/tsconfig.cy.json__tmpl__ new file mode 100644 index 00000000000000..5db18f12b7ef23 --- /dev/null +++ b/tools/generators/cypress-component-configuration/files/tsconfig.cy.json__tmpl__ @@ -0,0 +1,9 @@ +{ + "extends":"./tsconfig.json", + "compilerOptions":{ + "isolatedModules":false, + "types":["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"], + "lib":["ES2019", "dom"] + }, + "include":["**/*.cy.ts", "**/*.cy.tsx"] +} diff --git a/tools/generators/cypress-component-configuration/index.spec.ts b/tools/generators/cypress-component-configuration/index.spec.ts new file mode 100644 index 00000000000000..ce7411397e2a9a --- /dev/null +++ b/tools/generators/cypress-component-configuration/index.spec.ts @@ -0,0 +1,142 @@ +import { + addProjectConfiguration, + joinPathFragments, + NxJsonConfiguration, + readJson, + readNxJson, + serializeJson, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; + +import generator from './index'; + +describe(`cypress-component-configuration`, () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => {}; + + let tree: Tree; + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(noop); + jest.spyOn(console, 'info').mockImplementation(noop); + jest.spyOn(console, 'warn').mockImplementation(noop); + + tree = createTreeWithEmptyV1Workspace(); + }); + + it(`should not create component testing for application`, async () => { + const project = '@proj/app-one'; + tree = setupDummyPackage(tree, { name: project, projectType: 'application' }); + + await generator(tree, { project }); + + expect(tree.exists('apps/app-one/cypress.config.ts')).toBe(false); + }); + + it(`should setup cypress component testing for existing project`, async () => { + const project = '@proj/one'; + tree = setupDummyPackage(tree, { name: project }); + + await generator(tree, { project }); + + expect(tree.read('packages/one/cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(` + "import { baseConfig } from '@fluentui/scripts-cypress'; + + export default baseConfig; + " + `); + expect(readJson(tree, 'packages/one/tsconfig.json').references).toEqual( + expect.arrayContaining([ + { + path: './tsconfig.cy.json', + }, + ]), + ); + expect(readJson(tree, 'packages/one/tsconfig.lib.json').exclude).toEqual( + expect.arrayContaining(['**/*.cy.ts', '**/*.cy.tsx']), + ); + expect(readJson(tree, 'packages/one/tsconfig.cy.json')).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "isolatedModules": false, + "lib": Array [ + "ES2019", + "dom", + ], + "types": Array [ + "node", + "cypress", + "cypress-storybook/cypress", + "cypress-real-events", + ], + }, + "extends": "./tsconfig.json", + "include": Array [ + "**/*.cy.ts", + "**/*.cy.tsx", + ], + } + `); + + expect(readJson(tree, 'packages/one/package.json').scripts).toEqual( + expect.objectContaining({ + e2e: 'cypress run --component', + 'e2e:local': 'cypress open --component', + }), + ); + }); +}); + +function setupDummyPackage(tree: Tree, options: { name: string; projectType?: 'application' | 'library' }) { + const { name: pkgName, projectType = 'library' } = options; + + const workspaceConfig = readNxJson(tree) ?? {}; + const normalizedPkgName = getNormalizedPkgName({ pkgName, workspaceConfig }); + const paths = { + root: `${projectType === 'application' ? 'apps' : 'packages'}/${normalizedPkgName}`, + }; + + const templates = { + packageJson: { + name: pkgName, + version: '0.0.1', + typings: 'lib/index.d.ts', + main: 'lib-commonjs/index.js', + scripts: {}, + dependencies: {}, + devDependencies: {}, + }, + tsconfigs: { + root: { + references: [ + { + path: './tsconfig.lib.json', + }, + ], + }, + lib: { + extends: './tsconfig.json', + compilerOptions: {}, + exclude: ['./src/testing/**', '**/*.spec.ts', '**/*.spec.tsx', '**/*.test.ts', '**/*.test.tsx'], + }, + }, + }; + + tree.write(`${paths.root}/package.json`, serializeJson(templates.packageJson)); + tree.write(`${paths.root}/tsconfig.json`, serializeJson(templates.tsconfigs.root)); + tree.write(`${paths.root}/tsconfig.lib.json`, serializeJson(templates.tsconfigs.lib)); + tree.write(`${paths.root}/src/index.ts`, `export const greet = 'hello' `); + + addProjectConfiguration(tree, pkgName, { + root: paths.root, + sourceRoot: joinPathFragments(paths.root, 'src'), + projectType, + targets: {}, + }); + + return tree; +} + +function getNormalizedPkgName(options: { pkgName: string; workspaceConfig: NxJsonConfiguration }) { + return options.pkgName.replace(`@${options.workspaceConfig.npmScope}/`, ''); +} diff --git a/tools/generators/cypress-component-configuration/index.ts b/tools/generators/cypress-component-configuration/index.ts new file mode 100644 index 00000000000000..a417f05307c1b0 --- /dev/null +++ b/tools/generators/cypress-component-configuration/index.ts @@ -0,0 +1,35 @@ +import { Tree, formatFiles, names } from '@nrwl/devkit'; + +import { getProjectConfig, printUserLogs, UserLog } from '../../utils'; + +import type { CypressComponentConfigurationGeneratorSchema } from './schema'; + +import { addFiles } from './lib/add-files'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface _NormalizedSchema extends ReturnType {} + +export default async function (tree: Tree, schema: CypressComponentConfigurationGeneratorSchema) { + const userLog: UserLog = []; + const normalizedOptions = normalizeOptions(tree, schema); + + if (normalizedOptions.projectConfig.projectType === 'application') { + userLog.push({ type: 'warn', message: 'we dont support cypress component tests for applications' }); + printUserLogs(userLog); + return; + } + + addFiles(tree, normalizedOptions); + + await formatFiles(tree); +} + +function normalizeOptions(tree: Tree, options: CypressComponentConfigurationGeneratorSchema) { + const project = getProjectConfig(tree, { packageName: options.project }); + + return { + ...options, + ...project, + ...names(options.project), + }; +} diff --git a/tools/generators/cypress-component-configuration/lib/add-files.ts b/tools/generators/cypress-component-configuration/lib/add-files.ts new file mode 100644 index 00000000000000..8eca64ec75745f --- /dev/null +++ b/tools/generators/cypress-component-configuration/lib/add-files.ts @@ -0,0 +1,56 @@ +import { stripIndents, Tree, updateJson, generateFiles, joinPathFragments } from '@nrwl/devkit'; +import * as path from 'path'; + +import { PackageJson, TsConfig } from '../../../types'; +import { getProjectConfig } from '../../../utils'; +import { uniqueArray } from './utils'; + +const templates = { + config: stripIndents` + import { baseConfig } from '@fluentui/scripts-cypress'; + + export default baseConfig; + `, + tsconfig: { + extends: './tsconfig.json', + compilerOptions: { + isolatedModules: false, + types: ['node', 'cypress', 'cypress-storybook/cypress', 'cypress-real-events'], + lib: ['ES2019', 'dom'], + }, + include: ['**/*.cy.ts', '**/*.cy.tsx'], + }, +}; + +type Options = ReturnType; + +export function addFiles(tree: Tree, options: Options) { + generateFiles(tree, joinPathFragments(__dirname, '../files'), options.projectConfig.root, { tmpl: '' }); + + updateJson(tree, options.paths.tsconfig.main, (json: TsConfig) => { + json.references?.push({ + path: `./${path.basename(options.paths.tsconfig.cypress)}`, + }); + + return json; + }); + + // update lib ts with new exclude globs + updateJson(tree, options.paths.tsconfig.lib, (json: TsConfig) => { + json.exclude = json.exclude || []; + json.exclude.push(...['**/*.cy.ts', '**/*.cy.tsx']); + json.exclude = uniqueArray(json.exclude); + + return json; + }); + + updateJson(tree, options.paths.packageJson, (json: PackageJson) => { + json.scripts = json.scripts ?? {}; + json.scripts.e2e = 'cypress run --component'; + json.scripts['e2e:local'] = 'cypress open --component'; + + return json; + }); + + return tree; +} diff --git a/tools/generators/cypress-component-configuration/lib/utils.spec.ts b/tools/generators/cypress-component-configuration/lib/utils.spec.ts new file mode 100644 index 00000000000000..62640aacb1ace7 --- /dev/null +++ b/tools/generators/cypress-component-configuration/lib/utils.spec.ts @@ -0,0 +1,7 @@ +import { uniqueArray } from './utils'; + +describe(`utils`, () => { + it(`should create uniquie array`, () => { + expect(uniqueArray(['a', 'b', 'a', 'c', 'd', 'b'])).toEqual(['a', 'b', 'c', 'd']); + }); +}); diff --git a/tools/generators/cypress-component-configuration/lib/utils.ts b/tools/generators/cypress-component-configuration/lib/utils.ts new file mode 100644 index 00000000000000..773199330b2dbf --- /dev/null +++ b/tools/generators/cypress-component-configuration/lib/utils.ts @@ -0,0 +1,3 @@ +export function uniqueArray(value: T[]) { + return Array.from(new Set(value)); +} diff --git a/tools/generators/cypress-component-configuration/schema.json b/tools/generators/cypress-component-configuration/schema.json new file mode 100644 index 00000000000000..67c30c616a5377 --- /dev/null +++ b/tools/generators/cypress-component-configuration/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "cypress-component-configuration", + "type": "object", + "title": "Set up Cypress component testing for a project", + "description": "Set up Cypress component test for a project.", + "properties": { + "project": { + "type": "string", + "description": "The name of the project to add cypress component testing to", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What project should we add Cypress component testing to?" + } + }, + "required": ["project"] +} diff --git a/tools/generators/cypress-component-configuration/schema.ts b/tools/generators/cypress-component-configuration/schema.ts new file mode 100644 index 00000000000000..42f6fffc6cc2c8 --- /dev/null +++ b/tools/generators/cypress-component-configuration/schema.ts @@ -0,0 +1,6 @@ +export interface CypressComponentConfigurationGeneratorSchema { + /** + * The name of the project to add cypress component testing to + */ + project: string; +} diff --git a/tools/generators/migrate-converged-pkg/files/just-config.ts__tmpl__ b/tools/generators/migrate-converged-pkg/files/just-config.ts__tmpl__ index 4e711a1d5dea35..b7b2c9a33bf435 100644 --- a/tools/generators/migrate-converged-pkg/files/just-config.ts__tmpl__ +++ b/tools/generators/migrate-converged-pkg/files/just-config.ts__tmpl__ @@ -2,4 +2,4 @@ import { preset, task } from '@fluentui/scripts-tasks'; preset(); -task('build', 'build:react-components').cached?.();` +task('build', 'build:react-components').cached?.(); diff --git a/tools/generators/migrate-converged-pkg/index.ts b/tools/generators/migrate-converged-pkg/index.ts index 5086e159c02442..4b158af035e783 100644 --- a/tools/generators/migrate-converged-pkg/index.ts +++ b/tools/generators/migrate-converged-pkg/index.ts @@ -20,7 +20,8 @@ import * as path from 'path'; import * as os from 'os'; import * as ts from 'typescript'; -import { getTemplate } from './lib/utils'; +import { getTemplate, uniqueArray } from './lib/utils'; +import setupCypressComponentTesting from '../cypress-component-configuration'; import { PackageJson, TsConfig } from '../../types'; import { arePromptsEnabled, @@ -576,10 +577,6 @@ function hasConformanceSetup(tree: Tree, options: NormalizedSchema) { return tree.exists(options.paths.conformanceSetup); } -function uniqueArray(value: T[]) { - return Array.from(new Set(value)); -} - function updateNxWorkspace(tree: Tree, options: NormalizedSchema) { const packageType = getPackageType(tree, options); const tags = { @@ -921,49 +918,18 @@ function shouldSetupStorybook(tree: Tree, options: NormalizedSchema) { } } -function setupCypress(tree: Tree, options: NormalizedSchema) { - const template = { - exclude: ['**/*.cy.ts', '**/*.cy.tsx'], - }; +async function setupCypress(tree: Tree, options: NormalizedSchema) { + const shouldSetupCypress = tree.exists(options.paths.tsconfig.cypress); - if (!shouldSetupCypress(tree, options)) { + if (!shouldSetupCypress) { return tree; } - writeJson(tree, options.paths.tsconfig.cypress, templates.cypress.tsconfig); - - updateJson(tree, options.paths.tsconfig.main, (json: TsConfig) => { - json.references?.push({ - path: `./${path.basename(options.paths.tsconfig.cypress)}`, - }); - - return json; - }); - - // update lib ts with new exclude globs - updateJson(tree, options.paths.tsconfig.lib, (json: TsConfig) => { - json.exclude = json.exclude || []; - json.exclude.push(...template.exclude); - json.exclude = uniqueArray(json.exclude); - - return json; - }); - - updateJson(tree, options.paths.packageJson, (json: PackageJson) => { - json.scripts = json.scripts ?? {}; - json.scripts.e2e = 'cypress run --component'; - json.scripts['e2e:local'] = 'cypress open --component'; - - return json; - }); + await setupCypressComponentTesting(tree, { project: options.name }); return tree; } -function shouldSetupCypress(tree: Tree, options: NormalizedSchema) { - return tree.exists(options.paths.tsconfig.cypress); -} - function updateLocalJestConfig(tree: Tree, options: NormalizedSchema) { const jestSetupFilePath = options.paths.jestSetupFile; const packageType = getPackageType(tree, options); diff --git a/tools/generators/migrate-converged-pkg/lib/utils.ts b/tools/generators/migrate-converged-pkg/lib/utils.ts index f4c4b8039de8d8..d3d6bb27d68101 100644 --- a/tools/generators/migrate-converged-pkg/lib/utils.ts +++ b/tools/generators/migrate-converged-pkg/lib/utils.ts @@ -28,3 +28,7 @@ export function getTemplate(src: string, substitutions: Record) throw err; } } + +export function uniqueArray(value: T[]) { + return Array.from(new Set(value)); +} diff --git a/tools/utils.ts b/tools/utils.ts index 6007a8fba3d222..74be1bb389054d 100644 --- a/tools/utils.ts +++ b/tools/utils.ts @@ -100,6 +100,7 @@ export function getProjectConfig(tree: Tree, options: { packageName: string }) { rootPackageJson: joinPathFragments(projectConfig.root, 'src', 'unstable', 'package.json__tmpl__'), }, conformanceSetup: joinPathFragments(projectConfig.root, 'src', 'testing', 'isConformant.ts'), + cypressConfig: joinPathFragments(projectConfig.root, 'cypress.config.ts'), babelConfig: joinPathFragments(projectConfig.root, '.babelrc.json'), jestConfig: joinPathFragments(projectConfig.root, 'jest.config.js'), jestSetupFile: joinPathFragments(projectConfig.root, 'config', 'tests.js'),