diff --git a/packages/test-utils/src/shared.ts b/packages/test-utils/src/shared.ts index 187910a7..40adca1a 100644 --- a/packages/test-utils/src/shared.ts +++ b/packages/test-utils/src/shared.ts @@ -1,16 +1,31 @@ -import { IncomingMessage, RequestListener, ServerResponse, createServer } from 'node:http'; +import { IncomingMessage, RequestListener, ServerResponse, createServer as createHttpServer } from 'node:http'; +import { createServer as createNetServer } from 'node:net'; export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +export async function getRandomPort() { + const server = createNetServer(); + server.listen(0); + return new Promise((resolve, reject) => { + server.addListener('listening', () => { + const address = server.address(); + if (!address || typeof address === 'string') reject('Invalid address'); + else resolve(address.port); + server.close(); + }); + server.addListener('close', () => reject('Close')); + }); +} + export interface CreateStaticServerOptions { handler?: RequestListener; } export async function createStaticServer({ handler }: CreateStaticServerOptions = {}) { const serverHandler = handler || ((_req, res) => res.end(``)); - const server = createServer(serverHandler); + const server = createHttpServer(serverHandler); server.listen(0); return { server, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 626f6fa1..18b806e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -436,6 +436,9 @@ importers: '@webx-kit/test-utils': specifier: workspace:^ version: link:../../packages/test-utils + p-retry: + specifier: ^6.2.0 + version: 6.2.0 typescript: specifier: ^5.3.3 version: 5.3.3 @@ -4578,6 +4581,10 @@ packages: '@types/scheduler': 0.16.8 csstype: 3.1.3 + /@types/retry@0.12.2: + resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} + dev: true + /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} @@ -7473,6 +7480,11 @@ packages: engines: {node: '>= 0.4'} dev: true + /is-network-error@1.0.1: + resolution: {integrity: sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==} + engines: {node: '>=16'} + dev: true + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -8454,6 +8466,15 @@ packages: engines: {node: '>=6'} dev: true + /p-retry@6.2.0: + resolution: {integrity: sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==} + engines: {node: '>=16.17'} + dependencies: + '@types/retry': 0.12.2 + is-network-error: 1.0.1 + retry: 0.13.1 + dev: true + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -9530,6 +9551,11 @@ packages: signal-exit: 3.0.7 dev: true + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 50276e2b..64bb14f0 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -8,12 +8,13 @@ "@modern-js/utils": "^2.46.1", "@playwright/test": "^1.41.0", "@types/node": "^20.11.5", + "@webx-kit/chrome-types": "workspace:^", "@webx-kit/example-react": "workspace:^", "@webx-kit/example-solid": "workspace:^", "@webx-kit/example-svelte": "workspace:^", "@webx-kit/example-vue": "workspace:^", - "@webx-kit/chrome-types": "workspace:^", "@webx-kit/test-utils": "workspace:^", + "p-retry": "^6.2.0", "typescript": "^5.3.3" } } diff --git a/tests/e2e/tests/webx-test.ts b/tests/e2e/tests/webx-test.ts index 5a426b38..7e7440a6 100644 --- a/tests/e2e/tests/webx-test.ts +++ b/tests/e2e/tests/webx-test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert'; import path from 'node:path'; -import { fs, execa, stripAnsi, wait } from '@modern-js/utils'; -import { createWebxTest } from '@webx-kit/test-utils/playwright'; +import { dynamicImport, fs, execa, stripAnsi, wait } from '@modern-js/utils'; +import { createWebxTest, getRandomPort } from '@webx-kit/test-utils/playwright'; import { LaunchOptions } from '@playwright/test'; export function createLaunchOptions(packageName: string, launchOptions?: LaunchOptions) { @@ -43,7 +43,8 @@ export function startDev({ beforeAll, afterAll, afterEach }: typeof test) { baseDir = packageDir; const { stdout: dirtyFiles } = await execa('git', ['ls-files', '--modified'], { cwd: packageDir }); if (!!dirtyFiles) throw new Error('make sure all modifications have been staged'); - childProcess = execa('pnpm', ['dev'], { cwd: packageDir }); + const PORT = String(await getRandomPort()); + childProcess = execa('pnpm', ['dev'], { cwd: packageDir, env: { PORT } }); await Promise.race([ new Promise((resolve) => { const handler = (chunk: unknown) => { @@ -59,7 +60,11 @@ export function startDev({ beforeAll, afterAll, afterEach }: typeof test) { wait(20 * 1000).then((): Promise => Promise.reject('Timeout')), ]); }); - afterEach(() => execa('git', ['checkout', '.'], { cwd: baseDir })); + afterEach(async () => { + const { default: pRetry } = (await dynamicImport('p-retry')) as typeof import('p-retry'); + // tolerate `.git/index.lock` conflict + await pRetry(() => execa('git', ['checkout', '.'], { cwd: baseDir }), { retries: 3 }); + }); afterAll(() => childProcess.kill('SIGINT')); const resolvePath = (file: string) => path.resolve(baseDir, file);