diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 450c5e6db1..d6b09fae80 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,6 @@ jobs: env: PLATFORM_USER_NAME: ${{ secrets.PLATFORM_USER_NAME }} PLATFORM_USER_PASSWORD: ${{ secrets.PLATFORM_USER_PASSWORD }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -24,27 +23,11 @@ jobs: - name: Build CLI working-directory: ./packages/cli run: tsc -b - - name: Set the template version - run: | - node scripts/store-next-test-version.js --variable=UI_TEMPLATE_VERSION --output=$GITHUB_ENV - - name: Bump template versions - run: | - echo "E2E tests should target template version ${{ env.UI_TEMPLATE_VERSION }}" - echo "//registry.npmjs.org/:_authToken=${{ env.NPM_TOKEN }}" > ~/.npmrc - npm run npm:bump:template -- -- ${{ env.UI_TEMPLATE_VERSION }} - npm run npm:publish:template - node scripts/wait-for-published-packages.js -v ${{ env.UI_TEMPLATE_VERSION }} - - uses: actions/upload-artifact@v2 - if: failure() - with: - name: npm-logs - path: /home/runner/.npm/_logs/*debug.log - name: End-to-end Tests working-directory: ./packages/cli-e2e - run: | - echo "Running E2E tests with version ${{ env.UI_TEMPLATE_VERSION }}" - npm run test-e2e + run: npm run test-e2e - uses: actions/upload-artifact@v2 + if: ${{ always() }} with: name: test-screenshots path: ./packages/cli-e2e/screenshots diff --git a/packages/angular/src/search-token-server/rules/proxy.ts b/packages/angular/src/search-token-server/rules/proxy.ts index ada24be7c6..7eb9db204f 100644 --- a/packages/angular/src/search-token-server/rules/proxy.ts +++ b/packages/angular/src/search-token-server/rules/proxy.ts @@ -50,7 +50,7 @@ export function startProxyServerFromRootApp(_options: CoveoSchema): Rule { const packageJson = JSON.parse(packageJsonBuffer.toString()); packageJson.scripts['start'] = - 'concurrently "npm run start-server" "ng serve"'; + 'concurrently --raw "npm run start-server" "ng serve"'; packageJson.scripts['start-server'] = 'node ./scripts/start-server.js'; tree.overwrite( diff --git a/packages/cli-e2e/__tests__/angular.specs.ts b/packages/cli-e2e/__tests__/angular.specs.ts index 5f3218fffb..f4d65414bd 100644 --- a/packages/cli-e2e/__tests__/angular.specs.ts +++ b/packages/cli-e2e/__tests__/angular.specs.ts @@ -1,16 +1,23 @@ import retry from 'async-retry'; -import type {ChildProcessWithoutNullStreams} from 'child_process'; import type {HTTPRequest, Browser, Page} from 'puppeteer'; -import {setupUIProject, teardownUIProject} from '../utils/cli'; -import {getNewBrowser} from '../utils/browser'; +import { + answerPrompt, + getProjectPath, + isGenericYesNoPrompt, + setupUIProject, +} from '../utils/cli'; +import {captureScreenshots, getNewBrowser, openNewPage} from '../utils/browser'; import {isSearchRequest} from '../utils/platform'; +import {EOL} from 'os'; +import stripAnsi from 'strip-ansi'; +import {ProcessManager} from '../utils/processManager'; describe('ui', () => { describe('create:angular', () => { let browser: Browser; - const cliProcesses: ChildProcessWithoutNullStreams[] = []; + let processManager: ProcessManager; // TODO: CDX-90: Assign a dynamic port for the search token server on all ui projects const clientPort = '4200'; const projectName = 'angular-project'; @@ -20,15 +27,61 @@ describe('ui', () => { let page: Page; beforeAll(async () => { + processManager = new ProcessManager(); browser = await getNewBrowser(); - await setupUIProject('ui:create:angular', projectName, cliProcesses, { - flags: ['--defaults'], - timeout: 40e3, + const buildProcess = setupUIProject( + processManager, + 'ui:create:angular', + projectName, + { + flags: ['--defaults'], + } + ); + + buildProcess.stdout.on('data', async (data) => { + if (isGenericYesNoPrompt(data.toString())) { + await answerPrompt(`y${EOL}`, buildProcess); + } }); - }, 3e6); + + await Promise.race([ + new Promise((resolve) => { + buildProcess.on('exit', async () => { + resolve(); + }); + }), + new Promise((resolve) => { + buildProcess.stdout.on('data', (data) => { + if ( + /Happy hacking !/.test( + stripAnsi(data.toString()).replace(/\n/g, '') + ) + ) { + resolve(); + } + }); + }), + ]); + + const startServerProcess = processManager.spawn('npm', ['run', 'start'], { + cwd: getProjectPath(projectName), + }); + + await new Promise((resolve) => { + startServerProcess.stdout.on('data', async (data) => { + if ( + /Compiled successfully/.test( + stripAnsi(data.toString()).replace(/\n/g, '') + ) + ) { + resolve(); + } + }); + }); + }, 420e3); beforeEach(async () => { - page = await browser.newPage(); + page = await openNewPage(browser, page); page.on('request', (request: HTTPRequest) => { interceptedRequests.push(request); @@ -36,13 +89,14 @@ describe('ui', () => { }); afterEach(async () => { + await captureScreenshots(browser); page.removeAllListeners('request'); interceptedRequests = []; }); afterAll(async () => { await browser.close(); - await teardownUIProject(cliProcesses); + await processManager.killAllProcesses(); }, 5e3); it('should contain a search page section', async () => { @@ -54,9 +108,13 @@ describe('ui', () => { }); it('should retrieve the search token on the page load', async () => { + const tokenResponseListener = page.waitForResponse(tokenProxyEndpoint); + page.goto(searchPageEndpoint); - const tokenResponse = await page.waitForResponse(tokenProxyEndpoint); - expect(JSON.parse(await tokenResponse.text())).toMatchObject({ + + expect( + JSON.parse(await (await tokenResponseListener).text()) + ).toMatchObject({ token: expect.stringMatching(/^eyJhb.+/), }); }); diff --git a/packages/cli-e2e/__tests__/auth.specs.ts b/packages/cli-e2e/__tests__/auth.specs.ts index 331fe0bc86..1363d1a21d 100644 --- a/packages/cli-e2e/__tests__/auth.specs.ts +++ b/packages/cli-e2e/__tests__/auth.specs.ts @@ -1,34 +1,32 @@ -import {spawn} from 'child_process'; import retry from 'async-retry'; import type {Browser} from 'puppeteer'; -import type {ChildProcessWithoutNullStreams} from 'child_process'; -import { - answerPrompt, - CLI_EXEC_PATH, - isYesNoPrompt, - killCliProcess, -} from '../utils/cli'; +import {answerPrompt, CLI_EXEC_PATH, isYesNoPrompt} from '../utils/cli'; import {captureScreenshots, connectToChromeBrowser} from '../utils/browser'; +import {ProcessManager} from '../utils/processManager'; describe('auth', () => { describe('login', () => { let browser: Browser; - let cliProcess: ChildProcessWithoutNullStreams; + let processManager: ProcessManager; beforeAll(async () => { browser = await connectToChromeBrowser(); + processManager = new ProcessManager(); }); afterEach(async () => { await captureScreenshots(browser); - await killCliProcess(cliProcess); + await processManager.killAllProcesses(); }, 5e3); it('should open the platform page', async () => { // TODO CDX-98: Remove `-e=dev`. - cliProcess = spawn(CLI_EXEC_PATH, ['auth:login', '-e=dev']); + const cliProcess = processManager.spawn(CLI_EXEC_PATH, [ + 'auth:login', + '-e=dev', + ]); cliProcess.stderr.on('data', async (data) => { if (isYesNoPrompt(data.toString())) { await answerPrompt('n', cliProcess); diff --git a/packages/cli-e2e/__tests__/react.specs.ts b/packages/cli-e2e/__tests__/react.specs.ts index d868de2b31..3ee92112d1 100644 --- a/packages/cli-e2e/__tests__/react.specs.ts +++ b/packages/cli-e2e/__tests__/react.specs.ts @@ -1,16 +1,17 @@ import retry from 'async-retry'; -import type {ChildProcessWithoutNullStreams} from 'child_process'; import type {HTTPRequest, Browser, Page} from 'puppeteer'; +import stripAnsi from 'strip-ansi'; -import {setupUIProject, teardownUIProject} from '../utils/cli'; -import {getNewBrowser} from '../utils/browser'; +import {captureScreenshots, getNewBrowser, openNewPage} from '../utils/browser'; +import {getProjectPath, setupUIProject} from '../utils/cli'; import {isSearchRequest} from '../utils/platform'; +import {ProcessManager} from '../utils/processManager'; describe('ui', () => { describe('create:react', () => { let browser: Browser; - const cliProcesses: ChildProcessWithoutNullStreams[] = []; + let processManager: ProcessManager; // TODO: CDX-90: Assign a dynamic port for the search token server on all ui projects const clientPort = '3000'; const projectName = 'react-project'; @@ -21,27 +22,51 @@ describe('ui', () => { beforeAll(async () => { browser = await getNewBrowser(); - await setupUIProject('ui:create:react', projectName, cliProcesses, { - timeout: 30e3, + processManager = new ProcessManager(); + const buildProcess = setupUIProject( + processManager, + 'ui:create:react', + projectName + ); + + await new Promise((resolve) => { + buildProcess.on('exit', async () => { + resolve(); + }); }); - }, 3e6); - beforeEach(async () => { - page = await browser.newPage(); + const startServerProcess = processManager.spawn('npm', ['run', 'start'], { + cwd: getProjectPath(projectName), + }); + await new Promise((resolve) => { + startServerProcess.stdout.on('data', async (data) => { + if ( + /You can now view react-project in the browser/.test( + stripAnsi(data.toString()).replace(/\n/g, '') + ) + ) { + resolve(); + } + }); + }); + }, 15 * 60e3); + beforeEach(async () => { + page = await openNewPage(browser, page); page.on('request', (request: HTTPRequest) => { interceptedRequests.push(request); }); }); afterEach(async () => { + await captureScreenshots(browser); page.removeAllListeners('request'); interceptedRequests = []; }); afterAll(async () => { await browser.close(); - await teardownUIProject(cliProcesses); + await processManager.killAllProcesses(); }, 5e3); it('should contain a search page section', async () => { diff --git a/packages/cli-e2e/__tests__/vue.specs.ts b/packages/cli-e2e/__tests__/vue.specs.ts index 98381cccfa..62f47d95a7 100644 --- a/packages/cli-e2e/__tests__/vue.specs.ts +++ b/packages/cli-e2e/__tests__/vue.specs.ts @@ -1,16 +1,21 @@ import retry from 'async-retry'; - -import type {ChildProcessWithoutNullStreams} from 'child_process'; import type {HTTPRequest, Browser, Page} from 'puppeteer'; -import {setupUIProject, teardownUIProject} from '../utils/cli'; -import {captureScreenshots, getNewBrowser} from '../utils/browser'; +import { + answerPrompt, + getProjectPath, + isGenericYesNoPrompt, + setupUIProject, +} from '../utils/cli'; +import {captureScreenshots, getNewBrowser, openNewPage} from '../utils/browser'; import {isSearchRequest} from '../utils/platform'; +import stripAnsi from 'strip-ansi'; +import {EOL} from 'os'; +import {ProcessManager} from '../utils/processManager'; describe('ui', () => { describe('create:vue', () => { let browser: Browser; - const cliProcesses: ChildProcessWithoutNullStreams[] = []; // TODO: CDX-90: Assign a dynamic port for the search token server on all ui projects const clientPort = '8080'; const projectName = 'vue-project'; @@ -18,22 +23,63 @@ describe('ui', () => { const tokenProxyEndpoint = `http://localhost:${clientPort}/token`; let interceptedRequests: HTTPRequest[] = []; let page: Page; - - const openNewPage = async () => { - const newPage = await browser.newPage(); - if (page) { - await page.close(); - } - return newPage; - }; + let processManager: ProcessManager; beforeAll(async () => { + processManager = new ProcessManager(); browser = await getNewBrowser(); - await setupUIProject('ui:create:vue', projectName, cliProcesses); - }, 240e3); + + const buildProcess = setupUIProject( + processManager, + 'ui:create:vue', + projectName + ); + + buildProcess.stdout.on('data', async (data) => { + if (isGenericYesNoPrompt(data.toString())) { + await answerPrompt(`y${EOL}`, buildProcess); + return; + } + }); + + await Promise.race([ + new Promise((resolve) => { + buildProcess.on('exit', async () => { + resolve(); + }); + }), + new Promise((resolve) => { + buildProcess.stdout.on('data', (data) => { + if ( + /Happy hacking !/.test( + stripAnsi(data.toString()).replace(/\n/g, '') + ) + ) { + resolve(); + } + }); + }), + ]); + + const startServerProcess = processManager.spawn('npm', ['run', 'start'], { + cwd: getProjectPath(projectName), + }); + + await new Promise((resolve) => { + startServerProcess.stdout.on('data', async (data) => { + if ( + /App running at:/.test( + stripAnsi(data.toString()).replace(/\n/g, '') + ) + ) { + resolve(); + } + }); + }); + }, 420e3); beforeEach(async () => { - page = await openNewPage(); + page = await openNewPage(browser, page); page.on('request', (request: HTTPRequest) => { interceptedRequests.push(request); @@ -42,11 +88,13 @@ describe('ui', () => { afterEach(async () => { await captureScreenshots(browser); + page.removeAllListeners('request'); + interceptedRequests = []; }); afterAll(async () => { await browser.close(); - await teardownUIProject(cliProcesses); + await processManager.killAllProcesses(); }, 5e3); it('should contain a search page section', async () => { diff --git a/packages/cli-e2e/docker/config/config.yaml b/packages/cli-e2e/docker/config/config.yaml new file mode 100644 index 0000000000..4c2ab9c32c --- /dev/null +++ b/packages/cli-e2e/docker/config/config.yaml @@ -0,0 +1,47 @@ +# Verdaccio configuration +# - Look here for information about this file: +# https://verdaccio.org/docs/en/configuration.html +# - Look here for more config file examples: +# https://github.com/verdaccio/verdaccio/tree/master/conf + +# path to a directory with all packages +storage: /verdaccio/storage/data +# path to a directory with plugins to include +plugins: /verdaccio/plugins + +web: + enable: false + title: Verdaccio + +auth: + htpasswd: + file: /verdaccio/storage/htpasswd + +# a list of other known registries we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +packages: + # All templates packages are published and fetched from Verdaccio + # No uplink is tolerated, so if it ain't published on Verdaccio, it'll fail. + "@coveo/@(angular|vue-cli-plugin-typescript|cra-template)": + access: $all + publish: $all + unpublish: $all + + # For any other packages, we try verdaccio first (it does some caching) + # Otherwise, it tries npmjs. + "**": + access: $all + publish: $all + unpublish: $all + proxy: npmjs + +middlewares: + audit: + enabled: true + +# log settings +logs: + - { type: stdout, format: pretty, level: http } diff --git a/packages/cli-e2e/docker/docker-compose.yml b/packages/cli-e2e/docker/docker-compose.yml new file mode 100644 index 0000000000..0a51630c11 --- /dev/null +++ b/packages/cli-e2e/docker/docker-compose.yml @@ -0,0 +1,34 @@ +version: "3.9" + +services: + runner: + image: "coveo-cli-e2e-image" + container_name: "coveo-cli-e2e-container" + cap_add: + - "IPC_LOCK" + - "SYS_ADMIN" + env_file: "../.env" + privileged: true + ports: + - "9229:9229" + volumes: + - ../../..:/home/notGroot/cli + networks: + - node-network + command: "/bin/bash" + tty: true + stdin_open: true + verdaccio: + image: verdaccio/verdaccio + container_name: "verdaccio" + networks: + - node-network + environment: + - VERDACCIO_PORT=4873 + ports: + - "4873:4873" + volumes: + - "./config:/verdaccio/conf" +networks: + node-network: + driver: bridge diff --git a/packages/cli-e2e/entrypoints/common.js b/packages/cli-e2e/entrypoints/common.js index 86465be735..18e53dd39b 100644 --- a/packages/cli-e2e/entrypoints/common.js +++ b/packages/cli-e2e/entrypoints/common.js @@ -3,15 +3,17 @@ const {execSync, spawnSync} = require('child_process'); const {existsSync, mkdirSync, writeFileSync} = require('fs'); const DOCKER_IMAGE_NAME = 'coveo-cli-e2e-image'; +const composeProjectName = 'coveo-cli-e2e'; const DOCKER_CONTAINER_NAME = 'coveo-cli-e2e-container'; const repoHostPath = resolve(__dirname, ...new Array(3).fill('..')); const repoDockerPath = '/home/notGroot/cli'; const screenshotsHostPath = resolve(__dirname, '..', 'screenshots'); -const dockerFilePath = resolve(repoHostPath, 'packages', 'cli-e2e', 'docker'); +const dockerDirPath = resolve(repoHostPath, 'packages', 'cli-e2e', 'docker'); +const composeFilePath = resolve(dockerDirPath, 'docker-compose.yml'); const dockerEntryPoint = () => { if (isBash()) { - return '/bin/bash'; + return ''; } return join( repoDockerPath, @@ -53,18 +55,14 @@ const isImagePresent = () => { const ensureDockerImageIsPresent = () => { if (!isImagePresent()) { console.log('Building docker image'); - execSync(`docker build -t ${DOCKER_IMAGE_NAME} ${dockerFilePath}`, { + execSync(`docker build -t ${DOCKER_IMAGE_NAME} ${dockerDirPath}`, { stdio: 'ignore', }); } }; const createEnvFile = () => { - const credentials = [ - 'PLATFORM_USER_NAME', - 'PLATFORM_USER_PASSWORD', - 'UI_TEMPLATE_VERSION', - ]; + const credentials = ['PLATFORM_USER_NAME', 'PLATFORM_USER_PASSWORD']; if (existsSync('.env')) { return; @@ -78,32 +76,42 @@ const createEnvFile = () => { ); }; -const startDockerContainer = () => { +const startDockerCompose = () => { createEnvFile(); mkdirSync(screenshotsHostPath, {recursive: true}); return execSync( - `${process.env.CI ? 'sudo ' : ''}docker run \ - --name=${DOCKER_CONTAINER_NAME} \ - -v "${repoHostPath}:${repoDockerPath}" \ - -p "9229:9229" \ - -${process.argv[2] === '--bash' ? 'it' : 'i'} \ - --env-file .env \ - --cap-add=IPC_LOCK \ - --cap-add=SYS_ADMIN \ - --privileged \ - ${DOCKER_IMAGE_NAME} ${dockerEntryPoint()}`, - {stdio: ['inherit', 'inherit', 'inherit']} + `${ + process.env.CI ? 'sudo ' : '' + }docker-compose -f ${composeFilePath} -p ${composeProjectName} up --force-recreate -d`, + { + stdio: ['inherit', 'inherit', 'inherit'], + } ); }; -const cleanDockerContainer = () => - execSync(`docker container rm ${DOCKER_CONTAINER_NAME} -f`, { - stdio: 'ignore', - }); +const startTestRunning = () => { + return execSync( + `${process.env.CI ? 'sudo ' : ''}docker exec -${ + process.argv[2] === '--bash' ? 'it' : 'i' + } ${DOCKER_CONTAINER_NAME} /bin/bash ${dockerEntryPoint()} `, + { + stdio: ['inherit', 'inherit', 'inherit'], + } + ); +}; + +const stopDockerContainers = () => + execSync( + `docker-compose -f ${composeFilePath} -p ${composeProjectName} down`, + { + stdio: 'ignore', + } + ); module.exports = { DOCKER_CONTAINER_NAME, ensureDockerImageIsPresent, - startDockerContainer, - cleanDockerContainer, + startDockerCompose, + startTestRunning, + stopDockerContainers, }; diff --git a/packages/cli-e2e/entrypoints/dockerHeadless.sh b/packages/cli-e2e/entrypoints/dockerHeadless.sh index 4cae361584..9d22bef474 100755 --- a/packages/cli-e2e/entrypoints/dockerHeadless.sh +++ b/packages/cli-e2e/entrypoints/dockerHeadless.sh @@ -1,3 +1,5 @@ +#!/bin/bash + export DISPLAY=:1 Xvfb :1 -screen 0 1024x768x16 & sleep 1 @@ -7,8 +9,30 @@ rsync -r --exclude="node_modules" /home/notGroot/cli/* /home/notGroot/cli-copy/ cd /home/notGroot/cli-copy npm run setup +npm run build + +export UI_TEMPLATE_VERSION=0.0.0 +npm set registry http://verdaccio:4873 +yarn config set registry http://verdaccio:4873 +yarn config set -- --mutex network +yarn config set -- --install.silent true +yarn config set -- --silent true + +npm run npm:bump:template -- -- $UI_TEMPLATE_VERSION + +npm run npm:publish:template + +google-chrome --no-first-run --remote-debugging-port=9222 --disable-dev-shm-usage --window-size=1080,720 >/dev/null 2>&1 & \ + +node scripts/wait-for-published-packages.js + cd packages/cli-e2e -google-chrome --no-first-run --remote-debugging-port=9222 --disable-dev-shm-usage >/dev/null 2>&1 & \ -npm run-script jest -echo "Docker!\n" | sudo -S rsync -r /home/notGroot/cli-copy/packages/cli-e2e/screenshots/* /home/notGroot/cli/packages/cli-e2e/screenshots \ No newline at end of file +if npm run-script jest +then + echo "Docker!" | sudo -S rsync -r /home/notGroot/cli-copy/packages/cli-e2e/screenshots/* /home/notGroot/cli/packages/cli-e2e/screenshots + exit 0 +else + echo "Docker!" | sudo -S rsync -r /home/notGroot/cli-copy/packages/cli-e2e/screenshots/* /home/notGroot/cli/packages/cli-e2e/screenshots + exit 1 +fi diff --git a/packages/cli-e2e/entrypoints/dockerX11Entry.sh b/packages/cli-e2e/entrypoints/dockerX11Entry.sh index d92bfbd47a..4fe6cd61d9 100644 --- a/packages/cli-e2e/entrypoints/dockerX11Entry.sh +++ b/packages/cli-e2e/entrypoints/dockerX11Entry.sh @@ -6,7 +6,19 @@ rsync -r --exclude="node_modules" /home/notGroot/cli/* /home/notGroot/cli-copy/ cd /home/notGroot/cli-copy npm run setup -cd packages/cli-e2e +npm run build + +export UI_TEMPLATE_VERSION=0.0.0 +npm set registry http://verdaccio:4873 +yarn config set registry http://verdaccio:4873 + +npm run npm:bump:template -- -- $UI_TEMPLATE_VERSION + +npm run npm:publish:template + google-chrome --no-first-run --remote-debugging-port=9222 --disable-dev-shm-usage --window-size=1080,720 >/dev/null 2>&1 & \ +node scripts/wait-for-published-packages.js + +cd packages/cli-e2e npm run-script jest:debug diff --git a/packages/cli-e2e/entrypoints/entry.js b/packages/cli-e2e/entrypoints/entry.js index 0878b89a5a..98bde21810 100644 --- a/packages/cli-e2e/entrypoints/entry.js +++ b/packages/cli-e2e/entrypoints/entry.js @@ -1,14 +1,16 @@ const { ensureDockerImageIsPresent, - startDockerContainer, - cleanDockerContainer, + startDockerCompose, + startTestRunning, + stopDockerContainers, } = require('./common'); ensureDockerImageIsPresent(); try { - startDockerContainer(); + startDockerCompose(); + startTestRunning(); } finally { if (!process.env.CI) { - cleanDockerContainer(); + stopDockerContainers(); } } diff --git a/packages/cli-e2e/package.json b/packages/cli-e2e/package.json index dcae7b8b70..44414785e0 100644 --- a/packages/cli-e2e/package.json +++ b/packages/cli-e2e/package.json @@ -20,7 +20,7 @@ "test:headless": "node ./entrypoints/entry.js", "test:debug": "node ./entrypoints/entry.js --debug", "test:bash": "node ./entrypoints/entry.js --bash", - "jest": "jest --verbose", + "jest": "jest --verbose --runInBand", "jest:debug": "node --inspect=0.0.0.0:9229 node_modules/.bin/jest --runInBand" }, "bugs": { diff --git a/packages/cli-e2e/setup.ts b/packages/cli-e2e/setup.ts index 61494f83f2..cac30062ba 100644 --- a/packages/cli-e2e/setup.ts +++ b/packages/cli-e2e/setup.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -import {ChildProcessWithoutNullStreams} from 'child_process'; import {mkdirSync} from 'fs'; import type {Browser} from 'puppeteer'; import { @@ -8,14 +6,7 @@ import { SCREENSHOTS_PATH, } from './utils/browser'; import {clearAccessTokenFromConfig, loginWithOffice} from './utils/login'; - -declare global { - namespace NodeJS { - interface Global { - loginProcess: ChildProcessWithoutNullStreams | undefined; - } - } -} +import {ProcessManager} from './utils/processManager'; async function clearChromeBrowsingData(browser: Browser) { const pages = await browser.pages(); @@ -35,7 +26,8 @@ export default async function () { await clearChromeBrowsingData(browser); await clearAccessTokenFromConfig(); try { - global.loginProcess = await loginWithOffice(); + global.processManager = new ProcessManager(); + await loginWithOffice(); } catch (e) { await captureScreenshots(browser, 'jestSetup'); throw e; diff --git a/packages/cli-e2e/teardown.ts b/packages/cli-e2e/teardown.ts index 7768fafc78..f96ecb5a52 100644 --- a/packages/cli-e2e/teardown.ts +++ b/packages/cli-e2e/teardown.ts @@ -1,21 +1,10 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -import {ChildProcessWithoutNullStreams} from 'child_process'; import {closeAllPages, connectToChromeBrowser} from './utils/browser'; -import {killCliProcess} from './utils/cli'; - -declare global { - namespace NodeJS { - interface Global { - loginProcess: ChildProcessWithoutNullStreams | undefined; - } - } -} export default async function () { const browser = await connectToChromeBrowser(); const pageClosePromises = await closeAllPages(browser); - if (global.loginProcess) { - await killCliProcess(global.loginProcess); + if (global.processManager) { + await global.processManager.killAllProcesses(); } return Promise.all(pageClosePromises); } diff --git a/packages/cli-e2e/tsconfig.json b/packages/cli-e2e/tsconfig.json index 0a9bf60d09..e2a70e7158 100644 --- a/packages/cli-e2e/tsconfig.json +++ b/packages/cli-e2e/tsconfig.json @@ -9,5 +9,6 @@ "forceConsistentCasingInFileNames": true, "types": ["jest"], "lib": ["dom", "es2017"] - } + }, + "include": ["./**/*.ts"] } diff --git a/packages/cli-e2e/typings/global.d.ts b/packages/cli-e2e/typings/global.d.ts new file mode 100644 index 0000000000..1b4d3b2040 --- /dev/null +++ b/packages/cli-e2e/typings/global.d.ts @@ -0,0 +1,9 @@ +import type {ProcessManager} from '../utils/processManager'; + +declare global { + namespace NodeJS { + interface Global { + processManager: ProcessManager | undefined; + } + } +} diff --git a/packages/cli-e2e/utils/browser.ts b/packages/cli-e2e/utils/browser.ts index df642e531b..2cbaaf3a27 100644 --- a/packages/cli-e2e/utils/browser.ts +++ b/packages/cli-e2e/utils/browser.ts @@ -54,6 +54,14 @@ export async function getNewBrowser(): Promise { }); } +export async function openNewPage(browser: Browser, page?: Page | undefined) { + const newPage = await browser.newPage(); + if (page) { + await page.close(); + } + return newPage; +} + export async function captureScreenshots( browser: Browser, screenshotName?: string diff --git a/packages/cli-e2e/utils/cli.ts b/packages/cli-e2e/utils/cli.ts index 31cf8618c8..d6ff2e3586 100644 --- a/packages/cli-e2e/utils/cli.ts +++ b/packages/cli-e2e/utils/cli.ts @@ -1,27 +1,11 @@ -import {ensureDirSync} from 'fs-extra'; -import stripAnsi from 'strip-ansi'; -import {ChildProcessWithoutNullStreams, spawn} from 'child_process'; -import {resolve} from 'path'; -import {EOL} from 'os'; -import {join} from 'path'; +import type {ChildProcessWithoutNullStreams} from 'child_process'; +import {resolve, join} from 'path'; +import {mkdirSync} from 'fs'; +import {homedir} from 'os'; -export function killCliProcess(cliProcess: ChildProcessWithoutNullStreams) { - const waitForKill = new Promise((resolve) => { - cliProcess.on('close', () => resolve()); - }); - cliProcess.kill('SIGINT'); - return waitForKill; -} +import stripAnsi from 'strip-ansi'; -export function killCliProcessFamily( - cliProcessLeader: ChildProcessWithoutNullStreams -) { - const waitForKill = new Promise((resolve) => { - cliProcessLeader.on('close', () => resolve()); - }); - process.kill(-cliProcessLeader.pid); - return waitForKill; -} +import {ProcessManager} from './processManager'; export function isYesNoPrompt(data: string) { return data.trimEnd().toLowerCase().endsWith('(y/n):'); @@ -34,7 +18,7 @@ export function isGenericYesNoPrompt(data: string) { } catch (error) { console.log('Unable to strip ansi from string', error); } - return stripedData.match(/\(y\/n\)[\s:]*$/i) !== null; + return /\(y\/n\)[\s:]*$/i.test(stripedData); } export function answerPrompt( @@ -52,69 +36,35 @@ export function answerPrompt( export interface ISetupUIProjectOptionsArgs { flags?: string[]; - timeout?: number; } -export async function setupUIProject( +export function getProjectPath(projectName: string) { + const uiProjectFolderName = 'ui-projects'; + mkdirSync(join(homedir(), uiProjectFolderName), {recursive: true}); + return join(homedir(), uiProjectFolderName, projectName); +} + +export function setupUIProject( + processManager: ProcessManager, commandArgs: string, projectName: string, - cliProcesses: ChildProcessWithoutNullStreams[], options: ISetupUIProjectOptionsArgs = {} ) { - const uiProjectFolderName = '../ui-projects'; - ensureDirSync(uiProjectFolderName); - const defaultOptions: ISetupUIProjectOptionsArgs = {timeout: 15e3}; - options = Object.assign(defaultOptions, options); - - const createProjectPromise = new Promise((resolve) => { - const versionToTest = process.env.UI_TEMPLATE_VERSION; - let command = [commandArgs, projectName, ...(options.flags || [])]; - - if (versionToTest) { - command = command.concat(['-v', versionToTest]); - process.stdout.write( - `Testing with version ${versionToTest} of the template` - ); - } else { - process.stdout.write('Testing with published version of the template'); - } - - const buildProcess = spawn(CLI_EXEC_PATH, command, { - cwd: uiProjectFolderName, - }); - - buildProcess.stdout.on('close', async () => { - resolve(); - }); - - buildProcess.stdout.on('data', async (data) => { - if (isGenericYesNoPrompt(data.toString())) { - await answerPrompt(`y${EOL}`, buildProcess); - } - }); - }); - - await createProjectPromise; - - return new Promise((resolve) => { - const startServerProcess = spawn('npm', ['run', 'start'], { - cwd: join(uiProjectFolderName, projectName), - detached: true, - }); + const versionToTest = process.env.UI_TEMPLATE_VERSION; + let command = [commandArgs, projectName, ...(options.flags || [])]; + + if (versionToTest) { + command = command.concat(['-v', versionToTest]); + console.log(`Testing with version ${versionToTest} of the template`); + } else { + console.log('Testing with published version of the template'); + } - cliProcesses.push(startServerProcess); - setTimeout(() => { - resolve(); - }, options.timeout); + const buildProcess = processManager.spawn(CLI_EXEC_PATH, command, { + cwd: resolve(getProjectPath(projectName), '..'), }); -} -export async function teardownUIProject( - cliProcesses: ChildProcessWithoutNullStreams[] -) { - return Promise.all( - cliProcesses.map((cliProcess) => killCliProcessFamily(cliProcess)) - ); + return buildProcess; } export const CLI_EXEC_PATH = resolve(__dirname, '../../cli/bin/run'); diff --git a/packages/cli-e2e/utils/login.ts b/packages/cli-e2e/utils/login.ts index 553cdb796d..64d75d3e15 100644 --- a/packages/cli-e2e/utils/login.ts +++ b/packages/cli-e2e/utils/login.ts @@ -52,7 +52,10 @@ async function possiblyAcceptCustomerAgreement(page: Page) { } export function runLoginCommand() { - const cliProcess = spawn(CLI_EXEC_PATH, ['auth:login', '-e=dev']); + const cliProcess = global.processManager!.spawn(CLI_EXEC_PATH, [ + 'auth:login', + '-e=dev', + ]); cliProcess.stderr.on('data', async (data) => { if (isYesNoPrompt(data.toString())) { await answerPrompt('n', cliProcess); diff --git a/packages/cli-e2e/utils/processManager.ts b/packages/cli-e2e/utils/processManager.ts new file mode 100644 index 0000000000..dcb266d279 --- /dev/null +++ b/packages/cli-e2e/utils/processManager.ts @@ -0,0 +1,57 @@ +import { + ChildProcessWithoutNullStreams, + SpawnOptionsWithoutStdio, + spawn as nativeSpawn, +} from 'child_process'; + +export class ProcessManager { + private processes: Set; + constructor() { + this.processes = new Set(); + } + + public spawn( + command: string, + args?: ReadonlyArray, + options?: SpawnOptionsWithoutStdio + ): ChildProcessWithoutNullStreams { + const process = nativeSpawn(command, args, {detached: true, ...options}); + process.on('exit', this.onExit(process)); + this.processes.add(process); + return process; + } + + private onExit = (process: ChildProcessWithoutNullStreams) => () => { + this.processes.delete(process); + }; + + public async killAllProcesses() { + const promises: Promise[] = []; + const processIterator = this.processes.values(); + let current = processIterator.next(); + while (!current.done) { + const currentProcess = current.value; + currentProcess.removeAllListeners('exit'); + await new Promise((resolve) => { + promises.push( + new Promise((exit) => { + currentProcess.on('exit', () => { + exit(); + }); + if (!Number.isInteger(currentProcess.pid)) { + console.error( + `Process pid is not a number. Received pid: ${currentProcess.pid}` + ); + resolve(); + } + process.kill(-currentProcess.pid); + resolve(); + }) + ); + }); + current = processIterator.next(); + } + + return Promise.all(promises); + } +} diff --git a/scripts/.eslintrc b/scripts/.eslintrc new file mode 100644 index 0000000000..cbd6be70b8 --- /dev/null +++ b/scripts/.eslintrc @@ -0,0 +1,7 @@ +{ + "extends": "../.eslintrc", + "env": { + "es6": true + } + } + \ No newline at end of file diff --git a/scripts/store-next-test-version.js b/scripts/store-next-test-version.js deleted file mode 100644 index c88981df03..0000000000 --- a/scripts/store-next-test-version.js +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable no-undef */ -const {createWriteStream} = require('fs'); -const {spawn} = require('child_process'); -const yargs = require('yargs/yargs'); -const {hideBin} = require('yargs/helpers'); -const {inc, valid, gte} = require('semver'); -const { - getUiTemplates, - getPackageLastTestVersion, -} = require('./ui-template-utils'); - -async function getPackageNextTestVersion(packageName) { - const lastVersion = await getPackageLastTestVersion(packageName); - return inc(lastVersion, 'patch'); -} - -function getGreatestVersion(versions = []) { - const validVersions = versions.filter((v) => valid(v)); - const reducer = (previousVersion, currentVersion) => - gte(currentVersion, previousVersion) ? currentVersion : previousVersion; - return validVersions.reduce(reducer); -} - -async function getNextTestVersion() { - const templates = getUiTemplates(); - const allNextVersions = await Promise.all( - templates.map((p) => getPackageNextTestVersion(p)) - ); - - const greatestVersion = getGreatestVersion(allNextVersions); - return greatestVersion; -} - -function saveValueInEnvVariable(variable, value, output) { - return new Promise((resolve) => { - const logStream = createWriteStream(output, {flags: 'a'}); - const saveProcess = spawn('echo', [`${variable}=${value}`]); - - saveProcess.stdout.pipe(logStream); - - saveProcess.on('error', (error) => { - console.error(error); - }); - saveProcess.on('close', () => { - resolve(); - }); - }); -} - -async function main() { - const argv = yargs(hideBin(process.argv)).argv; - if (Boolean(argv.variable) === false) { - console.error( - 'The --variable flag is missing. Specify an environment variable to store the next test version.' - ); - return; - } - if (Boolean(argv.output) === false) { - console.error( - 'The --output flag is missing. Specify the output to redirect the environment variable assignment.' - ); - return; - } - const nextTestVersion = await getNextTestVersion(); - await saveValueInEnvVariable(argv.variable, nextTestVersion, argv.output); -} - -main(); diff --git a/scripts/ui-template-utils.js b/scripts/ui-template-utils.js index 728c284751..c373941f6f 100644 --- a/scripts/ui-template-utils.js +++ b/scripts/ui-template-utils.js @@ -1,7 +1,4 @@ -/* eslint-disable no-undef */ - -const {spawn} = require('child_process'); -const {valid} = require('semver'); +const {spawnSync} = require('child_process'); /** * All the UI templates used by 3rd-party CLIs. @@ -15,41 +12,16 @@ const getUiTemplates = () => [ '@coveo/angular', ]; -function cleanTestVersion(dirtyVersion) { - const versionRegex = /([0]+)\.([0]+)\.([0-9]+)/; - const match = dirtyVersion.match(versionRegex); - return match && match[0]; -} - -function getLastValidVersion(allVersions = []) { - let lastVersion = allVersions.pop(); - - while (allVersions.length > 0 && Boolean(valid(lastVersion)) == false) { - lastVersion = cleanTestVersion(allVersions.pop()); - } - return lastVersion; -} - -function getPackageLastTestVersion(packageName) { - let lastVersion = ''; - const spawnProcess = spawn('npm', [ +function getCiTestPackageVersion(packageName) { + const ciTestVersion = spawnSync('npm', [ 'show', - `${packageName}@0.0.*`, + `${packageName}@ci-test`, 'version', - '--json', ]); - - spawnProcess.stdout.on('data', (data) => { - const allVersions = data.toString().split('\n'); - lastVersion = getLastValidVersion(allVersions); - }); - - return new Promise((resolve) => { - spawnProcess.stdout.on('close', () => resolve(lastVersion)); - }); + return ciTestVersion.stdout; } module.exports = { - getPackageLastTestVersion, + getPackageLastTestVersion: getCiTestPackageVersion, getUiTemplates, }; diff --git a/scripts/wait-for-published-packages.js b/scripts/wait-for-published-packages.js index 3e13215db6..28b788bbd1 100644 --- a/scripts/wait-for-published-packages.js +++ b/scripts/wait-for-published-packages.js @@ -1,6 +1,3 @@ -/* eslint-disable no-undef */ -const yargs = require('yargs/yargs'); -const {hideBin} = require('yargs/helpers'); const {backOff} = require('exponential-backoff'); const { getPackageLastTestVersion, @@ -9,9 +6,7 @@ const { async function isLastTestVersionAvailable(packageName, lastVersion) { const response = await getPackageLastTestVersion(packageName); - return new Promise((resolve, reject) => { - return response === lastVersion ? resolve() : reject(); - }); + return response === lastVersion; } async function waitForPackage(packageName, version) { @@ -28,14 +23,10 @@ async function waitForPackage(packageName, version) { } async function main() { - const argv = yargs(hideBin(process.argv)).argv; const templates = getUiTemplates(); - if (Boolean(argv.v) === false) { - console.log('Missing -v flag'); - return; - } - - return await Promise.all(templates.map((t) => waitForPackage(t, argv.v))); + return await Promise.all( + templates.map((t) => waitForPackage(t, process.env.UI_TEMPLATE_VERSION)) + ); } main();