diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index d2f9dc9784..389f4b6f93 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -13,9 +13,9 @@ "backstage" ], "license": "Apache-2.0", - "main": "src/index.js", + "main": "src/index.ts", "scripts": { - "start": "ts-node .", + "start": "yarn ts-node --transpile-only --compiler-options '{\"module\":\"CommonJS\"}' .", "lint": "backstage-cli lint", "test": "backstage-cli test", "test:e2e": "yarn start" diff --git a/packages/e2e-test/src/helpers.js b/packages/e2e-test/src/helpers.ts similarity index 84% rename from packages/e2e-test/src/helpers.js rename to packages/e2e-test/src/helpers.ts index 580f53cbe8..783b7e3988 100644 --- a/packages/e2e-test/src/helpers.js +++ b/packages/e2e-test/src/helpers.ts @@ -14,16 +14,21 @@ * limitations under the License. */ -const { spawn, execFile: execFileCb } = require('child_process'); -const { promisify } = require('util'); +import { + spawn, + execFile as execFileCb, + SpawnOptions, + ChildProcess, +} from 'child_process'; +import { promisify } from 'util'; const execFile = promisify(execFileCb); const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; -function spawnPiped(cmd, options) { - function pipeWithPrefix(stream, prefix = '') { - return data => { +export function spawnPiped(cmd: string[], options?: SpawnOptions) { + function pipeWithPrefix(stream: NodeJS.WriteStream, prefix = '') { + return (data: Buffer) => { const prefixedMsg = data .toString('utf8') .trimRight() @@ -40,11 +45,11 @@ function spawnPiped(cmd, options) { child.on('error', handleError); const logPrefix = cmd.map(s => s.replace(/.+\//, '')).join(' '); - child.stdout.on( + child.stdout?.on( 'data', pipeWithPrefix(process.stdout, `[${logPrefix}].out: `), ); - child.stderr.on( + child.stderr?.on( 'data', pipeWithPrefix(process.stderr, `[${logPrefix}].err: `), ); @@ -52,7 +57,7 @@ function spawnPiped(cmd, options) { return child; } -async function runPlain(cmd, options) { +export async function runPlain(cmd: string[], options?: SpawnOptions) { try { const { stdout } = await execFile(cmd[0], cmd.slice(1), { ...options, @@ -70,8 +75,9 @@ async function runPlain(cmd, options) { } } -function handleError(err) { +export function handleError(err: Error & { code?: unknown }) { process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); + if (typeof err.code === 'number') { process.exit(err.code); } else { @@ -85,7 +91,7 @@ function handleError(err) { * .cancel() is available * @returns {Promise} Promise of resolution */ -function waitFor(fn) { +export function waitFor(fn: () => boolean) { return new Promise(resolve => { const handle = setInterval(() => { if (fn()) { @@ -97,7 +103,7 @@ function waitFor(fn) { }); } -async function waitForExit(child) { +export async function waitForExit(child: ChildProcess) { if (child.exitCode !== null) { throw new Error(`Child already exited with code ${child.exitCode}`); } @@ -113,10 +119,10 @@ async function waitForExit(child) { ); } -async function waitForPageWithText( - browser, - path, - text, +export async function waitForPageWithText( + browser: any, + path: string, + text: string, { intervalMs = 1000, maxLoadAttempts = 240, maxFindTextAttempts = 3 } = {}, ) { let loadAttempts = 0; @@ -163,16 +169,6 @@ async function waitForPageWithText( } } -function print(msg) { +export function print(msg: string) { return process.stdout.write(`${msg}\n`); } - -module.exports = { - spawnPiped, - runPlain, - handleError, - waitFor, - waitForExit, - waitForPageWithText, - print, -}; diff --git a/packages/e2e-test/src/index.js b/packages/e2e-test/src/index.ts similarity index 86% rename from packages/e2e-test/src/index.js rename to packages/e2e-test/src/index.ts index c4004f7c46..a455964f44 100644 --- a/packages/e2e-test/src/index.js +++ b/packages/e2e-test/src/index.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -const os = require('os'); -const fs = require('fs-extra'); -const fetch = require('node-fetch'); -const handlebars = require('handlebars'); -const killTree = require('tree-kill'); -const { resolve: resolvePath, join: joinPath } = require('path'); -const Browser = require('zombie'); -const { +import os from 'os'; +import fs from 'fs-extra'; +import fetch from 'node-fetch'; +import handlebars from 'handlebars'; +import killTree from 'tree-kill'; +import { resolve as resolvePath, join as joinPath } from 'path'; +import Browser from 'zombie'; +import { spawnPiped, runPlain, handleError, @@ -29,8 +29,8 @@ const { waitFor, waitForExit, print, -} = require('./helpers'); -const pgtools = require('pgtools'); +} from './helpers'; +import pgtools from 'pgtools'; async function main() { const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-')); @@ -59,7 +59,7 @@ async function main() { /** * Builds a dist workspace that contains the cli and core packages */ -async function buildDistWorkspace(workspaceName, rootDir) { +async function buildDistWorkspace(workspaceName: string, rootDir: string) { const workspaceDir = resolvePath(rootDir, workspaceName); await fs.ensureDir(workspaceDir); @@ -121,13 +121,20 @@ async function buildDistWorkspace(workspaceName, rootDir) { /** * Pin the yarn version in a directory to the one we're using in the Backstage repo */ -async function pinYarnVersion(dir) { +async function pinYarnVersion(dir: string) { const repoRoot = resolvePath(__dirname, '../../..'); const yarnRc = await fs.readFile(resolvePath(repoRoot, '.yarnrc'), 'utf8'); const yarnRcLines = yarnRc.split('\n'); const yarnPathLine = yarnRcLines.find(line => line.startsWith('yarn-path')); - const [, localYarnPath] = yarnPathLine.match(/"(.*)"/); + if (!yarnPathLine) { + throw new Error(`Unable to find 'yarn-path' in ${yarnRc}`); + } + const match = yarnPathLine.match(/"(.*)"/); + if (!match) { + throw new Error(`Invalid 'yarn-path' in ${yarnRc}`); + } + const [, localYarnPath] = match; const yarnPath = resolvePath(repoRoot, localYarnPath); await fs.writeFile(resolvePath(dir, '.yarnrc'), `yarn-path "${yarnPath}"\n`); @@ -136,7 +143,12 @@ async function pinYarnVersion(dir) { /** * Creates a new app inside rootDir called test-app, using packages from the workspaceDir */ -async function createApp(appName, isPostgres, workspaceDir, rootDir) { +async function createApp( + appName: string, + isPostgres: boolean, + workspaceDir: string, + rootDir: string, +) { const child = spawnPiped( [ 'node', @@ -150,20 +162,20 @@ async function createApp(appName, isPostgres, workspaceDir, rootDir) { try { let stdout = ''; - child.stdout.on('data', data => { + child.stdout?.on('data', data => { stdout = stdout + data.toString('utf8'); }); await waitFor(() => stdout.includes('Enter a name for the app')); - child.stdin.write(`${appName}\n`); + child.stdin?.write(`${appName}\n`); await waitFor(() => stdout.includes('Select database for the backend')); if (!isPostgres) { // Simulate down arrow press - child.stdin.write(`\u001B\u005B\u0042`); + child.stdin?.write(`\u001B\u005B\u0042`); } - child.stdin.write(`\n`); + child.stdin?.write(`\n`); print('Waiting for app create script to be done'); await waitForExit(child); @@ -205,7 +217,7 @@ async function createApp(appName, isPostgres, workspaceDir, rootDir) { /** * This points dependency resolutions into the workspace for each package that is present there */ -async function overrideModuleResolutions(appDir, workspaceDir) { +async function overrideModuleResolutions(appDir: string, workspaceDir: string) { const pkgJsonPath = resolvePath(appDir, 'package.json'); const pkgJson = await fs.readJson(pkgJsonPath); @@ -231,19 +243,19 @@ async function overrideModuleResolutions(appDir, workspaceDir) { /** * Uses create-plugin command to create a new plugin in the app */ -async function createPlugin(pluginName, appDir) { +async function createPlugin(pluginName: string, appDir: string) { const child = spawnPiped(['yarn', 'create-plugin'], { cwd: appDir, }); try { let stdout = ''; - child.stdout.on('data', data => { + child.stdout?.on('data', (data: Buffer) => { stdout = stdout + data.toString('utf8'); }); await waitFor(() => stdout.includes('Enter an ID for the plugin')); - child.stdin.write(`${pluginName}\n`); + child.stdin?.write(`${pluginName}\n`); // await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); // child.stdin.write('@someuser\n'); @@ -266,7 +278,7 @@ async function createPlugin(pluginName, appDir) { /** * Start serving the newly created app and make sure that the create plugin is rendering correctly */ -async function testAppServe(pluginName, appDir) { +async function testAppServe(pluginName: string, appDir: string) { const startApp = spawnPiped(['yarn', 'start'], { cwd: appDir, }); @@ -302,7 +314,7 @@ async function testAppServe(pluginName, appDir) { } /** Creates PG databases (drops if exists before) */ -async function createDB(database) { +async function createDB(database: string) { const config = { host: process.env.POSTGRES_HOST, port: process.env.POSTGRES_PORT, @@ -321,7 +333,7 @@ async function createDB(database) { /** * Start serving the newly created backend and make sure that all db migrations works correctly */ -async function testBackendStart(appDir, isPostgres) { +async function testBackendStart(appDir: string, isPostgres: boolean) { if (isPostgres) { print('Creating DBs'); await Promise.all( @@ -343,10 +355,10 @@ async function testBackendStart(appDir, isPostgres) { let stdout = ''; let stderr = ''; - child.stdout.on('data', data => { + child.stdout?.on('data', (data: Buffer) => { stdout = stdout + data.toString('utf8'); }); - child.stderr.on('data', data => { + child.stderr?.on('data', (data: Buffer) => { stderr = stderr + data.toString('utf8'); }); let successful = false; @@ -384,4 +396,4 @@ async function testBackendStart(appDir, isPostgres) { } process.on('unhandledRejection', handleError); -main(process.argv.slice(2)).catch(handleError); +main().catch(handleError); diff --git a/packages/e2e-test/src/types.d.ts b/packages/e2e-test/src/types.d.ts new file mode 100644 index 0000000000..0ae86490ee --- /dev/null +++ b/packages/e2e-test/src/types.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module 'zombie'; +declare module 'pgtools';