We'd like to use shared cypress commands, but that's currently not working because vite cannot resolve the import. By allowing to pass options to the vite config we have the possibility to add an alias or a plugin to fix this. This adds support for an **optional** `viteConfigOverrides` object. ## Current Behavior  cypress.config.ts: ```ts import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; export default defineConfig({ e2e: { ...nxE2EPreset(__filename, { cypressDir: 'cypress', bundler: 'vite' }), }, }); ``` ## Expected Behavior  cypress.config.ts: ```ts import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; const viteConfigOverrides = { resolve: { alias: { '@cypress-shared-commands': '../../../shared-cypress-commands', }, }, }; export default defineConfig({ e2e: { ...nxE2EPreset(__filename, { cypressDir: 'cypress', bundler: 'vite', viteConfigOverrides }), }, }); ``` ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
275 lines
7.6 KiB
TypeScript
275 lines
7.6 KiB
TypeScript
import { workspaceRoot } from '@nx/devkit';
|
|
import { dirname, join, relative } from 'path';
|
|
import { existsSync, lstatSync } from 'fs';
|
|
|
|
import vitePreprocessor from '../src/plugins/preprocessor-vite';
|
|
import { NX_PLUGIN_OPTIONS } from '../src/utils/constants';
|
|
|
|
import { spawn } from 'child_process';
|
|
import { request as httpRequest } from 'http';
|
|
import { request as httpsRequest } from 'https';
|
|
import type { InlineConfig } from 'vite';
|
|
|
|
// Importing the cypress type here causes the angular and next unit
|
|
// tests to fail when transpiling, it seems like the cypress types are
|
|
// clobbering jest's types. A bit weird. Leaving the commented out import
|
|
// and usage, as its helpful when modifying this code.
|
|
//
|
|
// import type * as Cypress from 'cypress';
|
|
|
|
interface BaseCypressPreset {
|
|
videosFolder: string;
|
|
screenshotsFolder: string;
|
|
chromeWebSecurity: boolean;
|
|
}
|
|
|
|
export interface NxComponentTestingOptions {
|
|
/**
|
|
* the component testing target name.
|
|
* this is only when customized away from the default value of `component-test`
|
|
* @example 'component-test'
|
|
*/
|
|
ctTargetName?: string;
|
|
buildTarget?: string;
|
|
bundler?: 'vite' | 'webpack';
|
|
compiler?: 'swc' | 'babel';
|
|
}
|
|
|
|
// The bundler is only used while generating the component testing configuration
|
|
// It cannot be changed after the configuration is generated
|
|
export interface NxComponentTestingPresetOptions
|
|
extends Omit<NxComponentTestingOptions, 'bundler'> {}
|
|
|
|
export function nxBaseCypressPreset(
|
|
pathToConfig: string,
|
|
options?: { testingType: 'component' | 'e2e' }
|
|
): BaseCypressPreset {
|
|
// used to set babel settings for react CT.
|
|
process.env.NX_CYPRESS_COMPONENT_TEST =
|
|
options?.testingType === 'component' ? 'true' : 'false';
|
|
// prevent from placing path outside the root of the workspace
|
|
// if they pass in a file or directory
|
|
const normalizedPath = lstatSync(pathToConfig).isDirectory()
|
|
? pathToConfig
|
|
: dirname(pathToConfig);
|
|
const projectPath = relative(workspaceRoot, normalizedPath);
|
|
const offset = relative(normalizedPath, workspaceRoot);
|
|
const videosFolder = join(offset, 'dist', 'cypress', projectPath, 'videos');
|
|
const screenshotsFolder = join(
|
|
offset,
|
|
'dist',
|
|
'cypress',
|
|
projectPath,
|
|
'screenshots'
|
|
);
|
|
|
|
return {
|
|
videosFolder,
|
|
screenshotsFolder,
|
|
chromeWebSecurity: false,
|
|
};
|
|
}
|
|
|
|
function startWebServer(webServerCommand: string) {
|
|
const serverProcess = spawn(webServerCommand, {
|
|
cwd: workspaceRoot,
|
|
shell: true,
|
|
// Detaching the process on unix will create a process group, allowing us to kill it later
|
|
// Windows is fine so we leave it attached to this process
|
|
detached: process.platform !== 'win32',
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
return () => {
|
|
if (process.platform === 'win32') {
|
|
serverProcess.kill();
|
|
} else {
|
|
// child.kill() does not work on linux
|
|
// process.kill will kill the whole process group on unix
|
|
process.kill(-serverProcess.pid, 'SIGKILL');
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* nx E2E Preset for Cypress
|
|
* @description
|
|
* this preset contains the base configuration
|
|
* for your e2e tests that nx recommends.
|
|
* you can easily extend this within your cypress config via spreading the preset
|
|
* @example
|
|
* export default defineConfig({
|
|
* e2e: {
|
|
* ...nxE2EPreset(__dirname)
|
|
* // add your own config here
|
|
* }
|
|
* })
|
|
*
|
|
*/
|
|
export function nxE2EPreset(
|
|
pathToConfig: string,
|
|
options?: NxCypressE2EPresetOptions
|
|
) {
|
|
const basePath = options?.cypressDir || 'src';
|
|
|
|
const baseConfig: any /*Cypress.EndToEndConfigOptions & {
|
|
[NX_PLUGIN_OPTIONS]: unknown;
|
|
}*/ = {
|
|
...nxBaseCypressPreset(pathToConfig),
|
|
fileServerFolder: '.',
|
|
supportFile: `${basePath}/support/e2e.{js,ts}`,
|
|
specPattern: `${basePath}/**/*.cy.{js,jsx,ts,tsx}`,
|
|
fixturesFolder: `${basePath}/fixtures`,
|
|
|
|
[NX_PLUGIN_OPTIONS]: {
|
|
webServerCommand: options?.webServerCommands?.default,
|
|
webServerCommands: options?.webServerCommands,
|
|
ciWebServerCommand: options?.ciWebServerCommand,
|
|
},
|
|
|
|
async setupNodeEvents(on, config) {
|
|
const webServerCommands =
|
|
config.env?.webServerCommands ?? options?.webServerCommands;
|
|
const webServerCommand =
|
|
config.env?.webServerCommand ?? webServerCommands?.default;
|
|
|
|
if (options?.bundler === 'vite') {
|
|
on('file:preprocessor', vitePreprocessor(options?.viteConfigOverrides));
|
|
}
|
|
|
|
if (!options?.webServerCommands) {
|
|
return;
|
|
}
|
|
|
|
if (!webServerCommand) {
|
|
return;
|
|
}
|
|
if (config.baseUrl && webServerCommand) {
|
|
if (await isServerUp(config.baseUrl)) {
|
|
if (
|
|
options?.webServerConfig?.reuseExistingServer === undefined
|
|
? true
|
|
: options.webServerConfig.reuseExistingServer
|
|
) {
|
|
console.log(
|
|
`Reusing the server already running on ${config.baseUrl}`
|
|
);
|
|
return;
|
|
} else {
|
|
throw new Error(
|
|
`Web server is already running at ${config.baseUrl}`
|
|
);
|
|
}
|
|
}
|
|
const killWebServer = startWebServer(webServerCommand);
|
|
|
|
on('after:run', () => {
|
|
killWebServer();
|
|
});
|
|
await waitForServer(config.baseUrl, options.webServerConfig);
|
|
}
|
|
},
|
|
};
|
|
|
|
return baseConfig;
|
|
}
|
|
|
|
function waitForServer(
|
|
url: string,
|
|
webServerConfig: WebServerConfig
|
|
): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
let pollTimeout: NodeJS.Timeout | null;
|
|
const { protocol } = new URL(url);
|
|
|
|
const timeoutDuration = webServerConfig?.timeout ?? 60 * 1000;
|
|
const timeout = setTimeout(() => {
|
|
clearTimeout(pollTimeout);
|
|
reject(
|
|
new Error(
|
|
`Web server failed to start in ${timeoutDuration}ms. This can be configured in cypress.config.ts.`
|
|
)
|
|
);
|
|
}, timeoutDuration);
|
|
|
|
const makeRequest = protocol === 'https:' ? httpsRequest : httpRequest;
|
|
|
|
function pollForServer() {
|
|
const request = makeRequest(url, () => {
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
});
|
|
|
|
request.on('error', () => {
|
|
pollTimeout = setTimeout(pollForServer, 100);
|
|
});
|
|
|
|
// Don't forget to end the request
|
|
request.end();
|
|
}
|
|
|
|
pollForServer();
|
|
});
|
|
}
|
|
|
|
function isServerUp(url: string) {
|
|
const { protocol } = new URL(url);
|
|
const makeRequest = protocol === 'https:' ? httpsRequest : httpRequest;
|
|
|
|
return new Promise((resolve) => {
|
|
const request = makeRequest(url, () => {
|
|
resolve(true);
|
|
});
|
|
|
|
request.on('error', () => {
|
|
resolve(false);
|
|
});
|
|
|
|
// Don't forget to end the request
|
|
request.end();
|
|
});
|
|
}
|
|
|
|
export interface WebServerConfig {
|
|
/**
|
|
* Timeout to wait for the webserver to start listening
|
|
*/
|
|
timeout?: number;
|
|
/**
|
|
* Reuse an existing web server if it exists
|
|
* If this is false, an error will be thrown if the server is already running
|
|
*/
|
|
reuseExistingServer?: boolean;
|
|
}
|
|
|
|
export type NxCypressE2EPresetOptions = {
|
|
bundler?: string;
|
|
/**
|
|
* The directory from the project root, where the cypress files (support, fixtures, specs) are located.
|
|
* i.e. 'cypress/' or 'src'
|
|
* default is 'src'
|
|
**/
|
|
cypressDir?: string;
|
|
|
|
/**
|
|
* A map of commandName -> command to start the web server for testing.
|
|
* Currently only default is read.
|
|
*/
|
|
webServerCommands?: Record<string, string>;
|
|
|
|
/**
|
|
* A command to start the web server - used for e2e tests distributed by Nx.
|
|
*/
|
|
ciWebServerCommand?: string;
|
|
|
|
/**
|
|
* Configures how the web server command is started and monitored.
|
|
*/
|
|
webServerConfig?: WebServerConfig;
|
|
|
|
/**
|
|
* Configure override inside the vite config
|
|
*/
|
|
viteConfigOverrides?: InlineConfig;
|
|
};
|