feat(testing): add continuous tasks support for jest e2e with node (#30675)

## Current Behavior
When generating `node` projects with an `e2e` project using Jest, we do
not supply any method for the node application to actually be started
before running the tests.

## Expected Behavior
Using Continuous Tasks, have the e2e project dependOn the serve of the
`node` project such that it is available for the e2e tests to run
against it.
This commit is contained in:
Colum Ferry 2025-04-11 14:22:54 +01:00 committed by Jason Jean
parent a1cd4e31ad
commit e5c7f6db18
11 changed files with 130 additions and 11 deletions

View File

@ -58,15 +58,11 @@ describe('Node Applications + webpack', () => {
async function runE2eTests(appName: string, port: number = 5000) {
process.env.PORT = `${port}`;
const childProcess = await runCommandUntil(`serve ${appName}`, (output) => {
return output.includes(`http://localhost:${port}`);
});
const result = runCLI(`e2e ${appName}-e2e --verbose`);
expect(result).toContain('Setting up...');
expect(result).toContain('Tearing down..');
expect(result).toContain('Successfully ran target e2e');
await promisifiedTreeKill(childProcess.pid, 'SIGKILL');
await killPort(port);
process.env.PORT = '';
}
@ -96,7 +92,7 @@ describe('Node Applications + webpack', () => {
`generate @nx/node:app apps/${koaApp} --framework=koa --port=7002 --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest`
);
runCLI(
`generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest`
`generate @nx/node:app apps/${nestApp} --framework=nest --port=7003 --bundler=webpack --no-interactive --linter=eslint --unitTestRunner=jest --e2eTestRunner=jest --verbose`
);
addLibImport(expressApp, testLib1);

View File

@ -337,13 +337,12 @@ module.exports = {
},
}
);
await killPorts(port);
await promisifiedTreeKill(p.pid, 'SIGKILL');
const e2eRsult = await runCLIAsync(`e2e ${nestapp}-e2e`);
expect(e2eRsult.combinedOutput).toContain('Test Suites: 1 passed, 1 total');
await killPorts(port);
await promisifiedTreeKill(p.pid, 'SIGKILL');
}, 120000);
it('should generate a nest application with docker', async () => {

View File

@ -444,6 +444,7 @@ describe('application generator', () => {
"e2e": {
"dependsOn": [
"@proj/myapp:build",
"@proj/myapp:serve",
],
"executor": "@nx/jest:jest",
"options": {

View File

@ -35,7 +35,9 @@
"@nx/devkit": "file:../devkit",
"@nx/jest": "file:../jest",
"@nx/js": "file:../js",
"@nx/eslint": "file:../eslint"
"@nx/eslint": "file:../eslint",
"tcp-port-used": "^1.0.2",
"kill-port": "^1.6.1"
},
"publishConfig": {
"access": "public"

View File

@ -934,6 +934,7 @@ describe('app', () => {
"e2e": {
"dependsOn": [
"@proj/myapp:build",
"@proj/myapp:serve",
],
"executor": "@nx/jest:jest",
"options": {

View File

@ -76,7 +76,7 @@ export async function e2eProjectGeneratorInternal(
jestConfig: `${options.e2eProjectRoot}/jest.config.ts`,
passWithNoTests: true,
},
dependsOn: [`${options.project}:build`],
dependsOn: [`${options.project}:build`, `${options.project}:serve`],
},
},
};
@ -93,7 +93,7 @@ export async function e2eProjectGeneratorInternal(
jestConfig: `${options.e2eProjectRoot}/jest.config.ts`,
passWithNoTests: true,
},
dependsOn: [`${options.project}:build`],
dependsOn: [`${options.project}:build`, `${options.project}:serve`],
},
},
});

View File

@ -1,3 +1,5 @@
import { waitForPortOpen } from '@nx/node/utils';
/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;
@ -5,6 +7,10 @@ module.exports = async function() {
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
console.log('\nSetting up...\n');
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
await waitForPortOpen(port, { host });
// Hint: Use `globalThis` to pass variables to global teardown.
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
};

View File

@ -1,7 +1,10 @@
import { killPort } from '@nx/node/utils';
/* eslint-disable */
module.exports = async function() {
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
// Hint: `globalThis` is shared between setup and teardown.
const port = process.env.PORT ? Number(process.env.PORT) : <%= port %>;
await killPort(port);
console.log(globalThis.__TEARDOWN_MESSAGE__);
};

View File

@ -0,0 +1,38 @@
import { logger } from '@nx/devkit';
import { check as portCheck } from 'tcp-port-used';
export const kill = require('kill-port');
/**
* Kills the process on the given port
* @param port
* @param killPortDelay
*/
export async function killPort(
port: number,
killPortDelay = 2500
): Promise<boolean> {
if (await portCheck(port)) {
let killPortResult;
try {
logger.info(`Attempting to close port ${port}`);
killPortResult = await kill(port);
await new Promise<void>((resolve) =>
setTimeout(() => resolve(), killPortDelay)
);
if (await portCheck(port)) {
logger.error(
`Port ${port} still open ${JSON.stringify(killPortResult)}`
);
} else {
logger.info(`Port ${port} successfully closed`);
return true;
}
} catch {
logger.error(`Port ${port} closing failed`);
}
return false;
} else {
return true;
}
}

View File

@ -0,0 +1,71 @@
import * as net from 'net';
import { logger } from '@nx/devkit';
interface WaitForPortOpenOptions {
/**
* The host to connect to
* @default 'localhost'
*/
host?: string;
/**
* The number of retries to attempt
* @default 120
*/
retries?: number;
/**
* The delay between retries
* @default 1000
*/
retryDelay?: number;
}
/**
* Waits for the given port to be open
* @param port
* @param options
*/
export function waitForPortOpen(
port: number,
options: WaitForPortOpenOptions = {}
): Promise<void> {
const host = options.host ?? 'localhost';
const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT'];
return new Promise((resolve, reject) => {
const checkPort = (retries = options.retries ?? 120) => {
const client = new net.Socket();
const cleanupClient = () => {
client.removeAllListeners('connect');
client.removeAllListeners('error');
client.end();
client.destroy();
client.unref();
};
client.once('connect', () => {
cleanupClient();
resolve();
});
client.once('error', (err) => {
if (retries === 0 || !allowedErrorCodes.includes(err['code'])) {
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
logger.info(
`Error connecting on ${host}:${port}: ${err['code'] || err}`
);
}
cleanupClient();
reject(err);
} else {
setTimeout(() => checkPort(retries - 1), options.retryDelay ?? 1000);
}
});
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
logger.info(`Connecting on ${host}:${port}`);
}
client.connect({ port, host });
};
checkPort();
});
}

2
packages/node/utils.ts Normal file
View File

@ -0,0 +1,2 @@
export { waitForPortOpen } from './src/utils/wait-for-port-open';
export { killPort } from './src/utils/kill-port';