diff --git a/tests/legacy-cli/e2e/tests/basic/rebuild.ts b/tests/legacy-cli/e2e/tests/basic/rebuild.ts index 486e6d3fb2c1..a1af3e836328 100644 --- a/tests/legacy-cli/e2e/tests/basic/rebuild.ts +++ b/tests/legacy-cli/e2e/tests/basic/rebuild.ts @@ -7,12 +7,15 @@ import { import { writeFile, writeMultipleFiles } from '../../utils/fs'; import { wait } from '../../utils/utils'; import fetch from 'node-fetch'; +import { findFreePort } from '../../utils/network'; const validBundleRegEx = / Compiled successfully./; -export default function () { +export default async function () { + const port = await findFreePort(); + return ( - execAndWaitForOutputToMatch('ng', ['serve'], validBundleRegEx) + execAndWaitForOutputToMatch('ng', ['serve', '--port', String(port)], validBundleRegEx) // Add a lazy module. .then(() => ng('generate', 'module', 'lazy', '--routing')) // Should trigger a rebuild with a new bundle. @@ -136,7 +139,7 @@ export default function () { ]), ) .then(() => wait(2000)) - .then(() => fetch('http://localhost:4200/main.js')) + .then(() => fetch(`http://localhost:${port}/main.js`)) .then((response) => response.text()) .then((body) => { if (!body.match(/\$\$_E2E_GOLDEN_VALUE_1/)) { @@ -158,7 +161,7 @@ export default function () { ]), ) .then(() => wait(2000)) - .then(() => fetch('http://localhost:4200/main.js')) + .then(() => fetch(`http://localhost:${port}/main.js`)) .then((response) => response.text()) .then((body) => { if (!body.match(/testingTESTING123/)) { @@ -174,7 +177,7 @@ export default function () { ]), ) .then(() => wait(2000)) - .then(() => fetch('http://localhost:4200/main.js')) + .then(() => fetch(`http://localhost:${port}/main.js`)) .then((response) => response.text()) .then((body) => { if (!body.match(/color:\s?blue/)) { diff --git a/tests/legacy-cli/e2e/tests/basic/serve.ts b/tests/legacy-cli/e2e/tests/basic/serve.ts index 9078c4779fd0..f0e893f2c538 100644 --- a/tests/legacy-cli/e2e/tests/basic/serve.ts +++ b/tests/legacy-cli/e2e/tests/basic/serve.ts @@ -5,20 +5,20 @@ import { ngServe } from '../../utils/project'; export default async function () { try { // Serve works without HMR - await ngServe('--no-hmr'); - await verifyResponse(); + const noHmrPort = await ngServe('--no-hmr'); + await verifyResponse(noHmrPort); await killAllProcesses(); // Serve works with HMR - await ngServe('--hmr'); - await verifyResponse(); + const hmrPort = await ngServe('--hmr'); + await verifyResponse(hmrPort); } finally { await killAllProcesses(); } } -async function verifyResponse(): Promise { - const response = await fetch('http://localhost:4200/'); +async function verifyResponse(port: number): Promise { + const response = await fetch(`http://localhost:${port}/`); if (!/<\/app-root>/.test(await response.text())) { throw new Error('Response does not match expected value.'); diff --git a/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts b/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts index 6217656a2a85..d5783b788363 100644 --- a/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts +++ b/tests/legacy-cli/e2e/tests/commands/serve/serve-path.ts @@ -3,17 +3,18 @@ import fetch from 'node-fetch'; import { killAllProcesses } from '../../../utils/process'; import { ngServe } from '../../../utils/project'; -export default function () { +export default async function () { // TODO(architect): Delete this test. It is now in devkit/build-angular. + const port = await ngServe('--serve-path', 'test/'); + return Promise.resolve() - .then(() => ngServe('--serve-path', 'test/')) - .then(() => fetch('http://localhost:4200/test', { headers: { 'Accept': 'text/html' } })) + .then(() => fetch(`http://localhost:${port}/test`, { headers: { 'Accept': 'text/html' } })) .then(async (response) => { assert.strictEqual(response.status, 200); assert.match(await response.text(), /<\/app-root>/); }) - .then(() => fetch('http://localhost:4200/test/abc', { headers: { 'Accept': 'text/html' } })) + .then(() => fetch(`http://localhost:${port}/test/abc`, { headers: { 'Accept': 'text/html' } })) .then(async (response) => { assert.strictEqual(response.status, 200); assert.match(await response.text(), /<\/app-root>/); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts index b55cffc22f8d..79854bfb193d 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts @@ -59,13 +59,17 @@ export default async function () { await ng('e2e', `--configuration=${lang}`, '--port=0'); // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, (baseHrefs[lang] as string) || '/'); + const { server, port, url } = await externalServer( + outputPath, + (baseHrefs[lang] as string) || '/', + ); try { await ng( 'e2e', + `--port=${port}`, `--configuration=${lang}`, '--dev-server-target=', - `--base-url=http://localhost:4200${baseHrefs[lang] || '/'}`, + `--base-url=${url}`, ); } finally { server.close(); @@ -89,13 +93,17 @@ export default async function () { await ng('e2e', `--configuration=${lang}`, '--port=0'); // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, '/test' + (baseHrefs[lang] || '/')); + const { server, port, url } = await externalServer( + outputPath, + '/test' + (baseHrefs[lang] || '/'), + ); try { await ng( 'e2e', + `--port=${port}`, `--configuration=${lang}`, '--dev-server-target=', - `--base-url=http://localhost:4200/test${baseHrefs[lang] || '/'}`, + `--base-url=${url}`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts index 70d49f5994cf..65fcfb0cbdbb 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts @@ -40,13 +40,14 @@ export default async function () { await ng('e2e', `--configuration=${lang}`, '--port=0'); // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); + const { server, port, url } = await externalServer(outputPath, `/${lang}/`); try { await ng( 'e2e', + `--port=${port}`, `--configuration=${lang}`, '--dev-server-target=', - `--base-url=http://localhost:4200/${lang}/`, + `--base-url=${url}`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts index 8e996d396e85..5dbfbc402b7e 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts @@ -44,13 +44,14 @@ export default async function () { await ng('e2e', `--configuration=${lang}`, '--port=0'); // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); + const { server, port, url } = await externalServer(outputPath, `/${lang}/`); try { await ng( 'e2e', + `--port=${port}`, `--configuration=${lang}`, '--dev-server-target=', - `--base-url=http://localhost:4200/${lang}/`, + `--base-url=${url}`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts index b1a9eaf307db..db45baf074b4 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts @@ -43,13 +43,14 @@ export default async function () { await ng('e2e', `--configuration=${lang}`, '--port=0'); // Execute Application E2E tests for a production build without dev server - const server = externalServer(outputPath, `/${lang}/`); + const { server, port, url } = await externalServer(outputPath, `/${lang}/`); try { await ng( 'e2e', + `--port=${port}`, `--configuration=${lang}`, '--dev-server-target=', - `--base-url=http://localhost:4200/${lang}/`, + `--base-url=${url}`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts index 82447344ad8e..91b598768302 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts @@ -2,6 +2,7 @@ import express from 'express'; import { join } from 'path'; import { getGlobalVariable } from '../../utils/env'; import { appendToFile, expectFileToMatch, writeFile } from '../../utils/fs'; +import { findFreePort } from '../../utils/network'; import { installWorkspacePackages } from '../../utils/packages'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; @@ -13,6 +14,7 @@ const snapshots = require('../../ng-snapshot/package.json'); export default async function () { // TODO: Re-enable pending further Ivy/Universal/i18n work return; + const port = await findFreePort(); // Setup i18n tests and config. await setupI18nConfig(); @@ -113,10 +115,10 @@ export default async function () { const { i18nApp } = (await import(serverBundle)) as { i18nApp(locale: string): express.Express; }; - const server = i18nApp(lang).listen(4200, 'localhost'); + const server = i18nApp(lang).listen(port, 'localhost'); try { // Execute without a devserver. - await ng('e2e', `--configuration=${lang}`, '--dev-server-target='); + await ng('e2e', `--port=${port}`, `--configuration=${lang}`, '--dev-server-target='); } finally { server.close(); } diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts index 297e4225d179..c95fd000059c 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts @@ -8,11 +8,13 @@ import { replaceInFile, writeFile, } from '../../utils/fs'; +import { findFreePort } from '../../utils/network'; import { installPackage } from '../../utils/packages'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; import { expectToFail } from '../../utils/utils'; import { readNgVersion } from '../../utils/version'; +import { externalServer } from './setup'; export default async function () { // TEMP: disable pending i18n updates @@ -154,9 +156,7 @@ export default async function () { await expectFileToExist(`${baseDir}/${lang}/ngsw.json`); // Ivy i18n doesn't yet work with `ng serve` so we must use a separate server. - const app = express(); - app.use(express.static(resolve(baseDir, lang))); - const server = app.listen(4200, 'localhost'); + const { server, port } = await externalServer(resolve(baseDir, lang)); try { // Add E2E test for locale await writeFile( @@ -180,7 +180,7 @@ export default async function () { ); // Execute without a devserver. - await ng('e2e', '--dev-server-target='); + await ng('e2e', '--dev-server-target=', `--port=${port}`); } finally { server.close(); } diff --git a/tests/legacy-cli/e2e/tests/i18n/setup.ts b/tests/legacy-cli/e2e/tests/i18n/setup.ts index d7954d6e5d59..816f3bd8f2ad 100644 --- a/tests/legacy-cli/e2e/tests/i18n/setup.ts +++ b/tests/legacy-cli/e2e/tests/i18n/setup.ts @@ -9,11 +9,13 @@ import { replaceInFile, writeFile, } from '../../utils/fs'; +import { findFreePort } from '../../utils/network'; import { installPackage } from '../../utils/packages'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; -import { expectToFail } from '../../utils/utils'; import { readNgVersion } from '../../utils/version'; +import { Server } from 'http'; +import { AddressInfo } from 'net'; // Configurations for each locale. export const baseDir = 'dist/test-project'; @@ -67,13 +69,33 @@ export const langTranslations = [ ]; export const sourceLocale = langTranslations[0].lang; -export const externalServer = (outputPath: string, baseUrl = '/') => { +export interface ExternalServer { + readonly server: Server; + readonly port: number; + readonly url: string; +} + +/** + * Create an `express` `http.Server` listening on a random port. + * + * Call .close() on the server return value to close the server. + */ +export async function externalServer(outputPath: string, baseUrl = '/'): Promise { const app = express(); app.use(baseUrl, express.static(resolve(outputPath))); - // call .close() on the return value to close the server. - return app.listen(4200, 'localhost'); -}; + return new Promise((resolve) => { + const server = app.listen(0, 'localhost', () => { + const { port } = server.address() as AddressInfo; + + resolve({ + server, + port, + url: `http://localhost:${port}${baseUrl}`, + }); + }); + }); +} export const formats = { 'xlf': { diff --git a/tests/legacy-cli/e2e/tests/misc/fallback.ts b/tests/legacy-cli/e2e/tests/misc/fallback.ts index bb3b27b150bc..925d694a4800 100644 --- a/tests/legacy-cli/e2e/tests/misc/fallback.ts +++ b/tests/legacy-cli/e2e/tests/misc/fallback.ts @@ -12,7 +12,7 @@ export default function () { return ( Promise.resolve() .then(() => ngServe()) - .then(() => fetch('http://localhost:4200/', { headers: { 'Accept': 'text/html' } })) + .then((port) => fetch(`http://localhost:${port}/`, { headers: { 'Accept': 'text/html' } })) .then(async (response) => { assert.strictEqual(response.status, 200); assert.match(await response.text(), /<\/app-root>/); @@ -27,7 +27,7 @@ export default function () { }), ) .then(() => ngServe()) - .then(() => fetch('http://localhost:4200/', { headers: { 'Accept': 'text/html' } })) + .then((port) => fetch(`http://localhost:${port}/`, { headers: { 'Accept': 'text/html' } })) .then(async (response) => { assert.strictEqual(response.status, 200); assert.match(await response.text(), /<\/app-root>/); diff --git a/tests/legacy-cli/e2e/tests/misc/proxy-config.ts b/tests/legacy-cli/e2e/tests/misc/proxy-config.ts index ff860a0a78ae..edc0619ad76c 100644 --- a/tests/legacy-cli/e2e/tests/misc/proxy-config.ts +++ b/tests/legacy-cli/e2e/tests/misc/proxy-config.ts @@ -34,7 +34,7 @@ export default function () { return Promise.resolve() .then(() => writeFile(proxyConfigFile, JSON.stringify(proxyConfig, null, 2))) .then(() => ngServe('--proxy-config', proxyConfigFile)) - .then(() => fetch('http://localhost:4200/api/test')) + .then((port) => fetch(`http://localhost:${port}/api/test`)) .then(async (response) => { assert.strictEqual(response.status, 200); assert.match(await response.text(), /TEST_API_RETURN/); diff --git a/tests/legacy-cli/e2e/tests/misc/public-host.ts b/tests/legacy-cli/e2e/tests/misc/public-host.ts index bf1e74448694..6b5aa3c52dfc 100644 --- a/tests/legacy-cli/e2e/tests/misc/public-host.ts +++ b/tests/legacy-cli/e2e/tests/misc/public-host.ts @@ -11,12 +11,12 @@ export default function () { .filter((ni) => ni?.family === 'IPv4' && !ni?.internal) .map((ni) => ni!.address) .shift(); - const publicHost = `${firstLocalIp}:4200`; + const publicHost = `${firstLocalIp}`; const localAddress = `http://${publicHost}`; return Promise.resolve() .then(() => ngServe('--host=0.0.0.0', `--public-host=${publicHost}`)) - .then(() => fetch(localAddress)) + .then((port) => fetch(`${localAddress}:${port}`)) .then((response) => response.text()) .then((body) => { if (!body.match(/<\/app-root>/)) { @@ -25,7 +25,7 @@ export default function () { }) .then(() => killAllProcesses()) .then(() => ngServe('--host=0.0.0.0', `--disable-host-check`)) - .then(() => fetch(localAddress)) + .then((port) => fetch(`${localAddress}:${port}`)) .then((response) => response.text()) .then((body) => { if (!body.match(/<\/app-root>/)) { @@ -35,7 +35,7 @@ export default function () { .then(() => killAllProcesses()) .then(() => ngServe('--host=0.0.0.0', `--public-host=${localAddress}`)) - .then(() => fetch(localAddress)) + .then((port) => fetch(`${localAddress}:${port}`)) .then((response) => response.text()) .then((body) => { if (!body.match(/<\/app-root>/)) { @@ -44,7 +44,7 @@ export default function () { }) .then(() => killAllProcesses()) .then(() => ngServe('--host=0.0.0.0', `--public-host=${firstLocalIp}`)) - .then(() => fetch(localAddress)) + .then((port) => fetch(`${localAddress}:${port}`)) .then((response) => response.text()) .then((body) => { if (!body.match(/<\/app-root>/)) { diff --git a/tests/legacy-cli/e2e/tests/misc/ssl-default.ts b/tests/legacy-cli/e2e/tests/misc/ssl-default.ts index 0dd010337a60..c985df57b798 100644 --- a/tests/legacy-cli/e2e/tests/misc/ssl-default.ts +++ b/tests/legacy-cli/e2e/tests/misc/ssl-default.ts @@ -8,9 +8,9 @@ export default async function () { // TODO(architect): Delete this test. It is now in devkit/build-angular. try { - await ngServe('--ssl', 'true'); + const port = await ngServe('--ssl', 'true'); - const response = await fetch('https://localhost:4200/', { + const response = await fetch(`https://localhost:${port}/`, { agent: new Agent({ rejectUnauthorized: false }), }); diff --git a/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts b/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts index 4ff12a5b2232..d1f107cd0e4e 100644 --- a/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts +++ b/tests/legacy-cli/e2e/tests/misc/ssl-with-cert.ts @@ -9,7 +9,7 @@ export default async function () { // TODO(architect): Delete this test. It is now in devkit/build-angular. try { - await ngServe( + const port = await ngServe( '--ssl', 'true', '--ssl-key', @@ -18,7 +18,7 @@ export default async function () { assetDir('ssl/server.crt'), ); - const response = await fetch('https://localhost:4200/', { + const response = await fetch(`https://localhost:${port}/`, { agent: new Agent({ rejectUnauthorized: false }), }); diff --git a/tests/legacy-cli/e2e/utils/network.ts b/tests/legacy-cli/e2e/utils/network.ts new file mode 100644 index 000000000000..e7cff34eec9c --- /dev/null +++ b/tests/legacy-cli/e2e/utils/network.ts @@ -0,0 +1,13 @@ +import { AddressInfo, createServer } from 'net'; + +export function findFreePort() { + return new Promise((resolve, reject) => { + const srv = createServer(); + srv.once('listening', () => { + const port = (srv.address() as AddressInfo).port; + srv.close((e) => (e ? reject(e) : resolve(port))); + }); + srv.once('error', (e) => srv.close(() => reject(e))); + srv.listen(); + }); +} diff --git a/tests/legacy-cli/e2e/utils/project.ts b/tests/legacy-cli/e2e/utils/project.ts index f55194f3b8aa..3f750464dd3e 100644 --- a/tests/legacy-cli/e2e/utils/project.ts +++ b/tests/legacy-cli/e2e/utils/project.ts @@ -6,6 +6,7 @@ import { packages } from '../../../../lib/packages'; import { getGlobalVariable } from './env'; import { prependToFile, readFile, replaceInFile, writeFile } from './fs'; import { gitCommit } from './git'; +import { findFreePort } from './network'; import { installWorkspacePackages } from './packages'; import { exec, execAndWaitForOutputToMatch, git, ng } from './process'; @@ -23,8 +24,16 @@ export function updateTsConfig(fn: (json: any) => any | void) { return updateJsonFile('tsconfig.json', fn); } -export function ngServe(...args: string[]) { - return execAndWaitForOutputToMatch('ng', ['serve', ...args], / Compiled successfully./); +export async function ngServe(...args: string[]) { + const port = await findFreePort(); + + await execAndWaitForOutputToMatch( + 'ng', + ['serve', '--port', String(port), ...args], + / Compiled successfully./, + ); + + return port; } export async function prepareProjectForE2e(name: string) { const argv: yargsParser.Arguments = getGlobalVariable('argv'); diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts index 5dab8c769449..1cbe21956ede 100644 --- a/tests/legacy-cli/e2e_runner.ts +++ b/tests/legacy-cli/e2e_runner.ts @@ -7,9 +7,9 @@ import * as path from 'path'; import { getGlobalVariable, setGlobalVariable } from './e2e/utils/env'; import { gitClean } from './e2e/utils/git'; import { createNpmRegistry } from './e2e/utils/registry'; -import { AddressInfo, createServer } from 'net'; import { launchTestProcess } from './e2e/utils/process'; import { join } from 'path'; +import { findFreePort } from './e2e/utils/network'; Error.stackTraceLimit = Infinity; @@ -254,15 +254,3 @@ function printFooter(testName: string, type: 'setup' | 'initializer' | 'test', s console.log(colors.green(`Last ${type} took `) + colors.bold.blue('' + t) + colors.green('s...')); console.log(''); } - -function findFreePort() { - return new Promise((resolve, reject) => { - const srv = createServer(); - srv.once('listening', () => { - const port = (srv.address() as AddressInfo).port; - srv.close((e) => (e ? reject(e) : resolve(port))); - }); - srv.once('error', (e) => srv.close(() => reject(e))); - srv.listen(); - }); -}