From ceb716c3b4c7df5b9cfb11845c75d0ea1377e1a7 Mon Sep 17 00:00:00 2001 From: Marcus Eide Date: Tue, 14 Apr 2020 08:55:07 +0200 Subject: [PATCH] Add create-app to e2e and move common functionality into seperate modules --- scripts/cli-e2e-test.js | 158 ++++++------------------------------ scripts/createTestApp.js | 41 ++++++++++ scripts/createTestPlugin.js | 44 ++++++++++ scripts/helpers.js | 137 +++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+), 135 deletions(-) create mode 100644 scripts/createTestApp.js create mode 100644 scripts/createTestPlugin.js create mode 100644 scripts/helpers.js diff --git a/scripts/cli-e2e-test.js b/scripts/cli-e2e-test.js index 48058550d9..e852f73ed0 100644 --- a/scripts/cli-e2e-test.js +++ b/scripts/cli-e2e-test.js @@ -15,11 +15,18 @@ */ const { resolve: resolvePath } = require('path'); -const childProcess = require('child_process'); -const { spawn } = childProcess; const Browser = require('zombie'); -const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; +const { + spawnPiped, + handleError, + waitForPageWithText, + waitForExit, + print, +} = require('./helpers'); + +const createTestApp = require('./createTestApp'); +const createTestPlugin = require('./createTestPlugin'); Browser.localhost('localhost', 3000); @@ -29,154 +36,35 @@ async function main() { const projectDir = resolvePath(__dirname, '..'); process.chdir(projectDir); - const start = spawnPiped(['yarn', 'start']); + await createTestApp(); + + const appDir = resolvePath(projectDir, 'test-app'); + process.chdir(appDir); + + print('Starting the app'); + const startApp = spawnPiped(['yarn', 'start']); try { const browser = new Browser(); + await createTestPlugin(); await waitForPageWithText(browser, '/', 'Welcome to Backstage'); - print('Backstage loaded correctly, creating plugin'); - - const createPlugin = spawnPiped(['yarn', 'create-plugin']); - - let stdout = ''; - createPlugin.stdout.on('data', data => { - stdout = stdout + data.toString('utf8'); - }); - - await waitFor(() => stdout.includes('Enter an ID for the plugin')); - createPlugin.stdin.write('test-plugin\n'); - - await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); - createPlugin.stdin.write('@someuser\n'); - - print('Waiting for plugin create script to be done'); - await waitForExit(createPlugin); - - print('Plugin create script is done, waiting for plugin page to load'); await waitForPageWithText( browser, '/test-plugin', 'Welcome to test-plugin!', ); - print('Test plugin loaded correctly, exiting'); + print('Both App and Plugin loaded correctly'); } finally { - start.kill(); + startApp.kill(); } - await waitForExit(start); + await waitForExit(startApp); + + print('All tests done'); process.exit(0); } -function waitFor(fn) { - return new Promise(resolve => { - const handle = setInterval(() => { - if (fn()) { - clearInterval(handle); - resolve(); - return; - } - }, 100); - }); -} - -function print(msg) { - return process.stdout.write(`${msg}\n`); -} - -async function waitForExit(child) { - if (child.exitCode !== null) { - throw new Error(`Child already exited with code ${child.exitCode}`); - } - await new Promise((resolve, reject) => - child.once('exit', code => { - if (code) { - reject(new Error(`Child exited with code ${code}`)); - } else { - resolve(); - } - }), - ); -} - -function spawnPiped(cmd, options) { - function pipeWithPrefix(stream, prefix = '') { - return data => { - const prefixedMsg = data - .toString('utf8') - .trimRight() - .replace(/^/gm, prefix); - stream.write(`${prefixedMsg}\n`, 'utf8'); - }; - } - - const child = spawn(cmd[0], cmd.slice(1), { - stdio: 'pipe', - shell: true, - ...options, - }); - child.on('error', handleError); - child.on('exit', code => { - if (code) { - print(`Child '${cmd.join(' ')}' exited with code ${code}`); - process.exit(code); - } - }); - child.stdout.on( - 'data', - pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), - ); - child.stderr.on( - 'data', - pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), - ); - - return child; -} - -async function waitForPageWithText( - browser, - path, - text, - { intervalMs = 1000, maxAttempts = 120 } = {}, -) { - let attempts = 0; - for (;;) { - try { - await new Promise(resolve => setTimeout(resolve, intervalMs)); - await browser.visit(path); - break; - } catch (error) { - if (error.message.match(EXPECTED_LOAD_ERRORS)) { - attempts++; - if (attempts > maxAttempts) { - throw new Error( - `Failed to load page '${path}', max number of attempts reached`, - ); - } - } else { - throw error; - } - } - } - - const escapedText = text.replace(/"/g, '\\"'); - browser.assert.evaluate( - `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, - true, - `expected to find text ${text}`, - ); -} - -function handleError(err) { - process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); - if (typeof err.code === 'number') { - process.exit(err.code); - } else { - process.exit(1); - } -} - process.on('unhandledRejection', handleError); main(process.argv.slice(2)).catch(handleError); diff --git a/scripts/createTestApp.js b/scripts/createTestApp.js new file mode 100644 index 0000000000..89fa023436 --- /dev/null +++ b/scripts/createTestApp.js @@ -0,0 +1,41 @@ +/* + * 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. + */ + +const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); + +async function createTestApp() { + print('Creating a Backstage App'); + const createApp = spawnPiped(['yarn', 'create-app']); + + try { + let stdout = ''; + createApp.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter a name for the app')); + createApp.stdin.write('test-app\n'); + + print('Waiting for app create script to be done'); + await waitForExit(createApp); + + print('Test app created'); + } finally { + createApp.kill(); + } +} + +module.exports = createTestApp; diff --git a/scripts/createTestPlugin.js b/scripts/createTestPlugin.js new file mode 100644 index 0000000000..c204067ae3 --- /dev/null +++ b/scripts/createTestPlugin.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +const { spawnPiped, waitFor, waitForExit, print } = require('./helpers'); + +async function createTestPlugin() { + print('Creating a Backstage Plugin'); + const createPlugin = spawnPiped(['yarn', 'create-plugin']); + + try { + let stdout = ''; + createPlugin.stdout.on('data', data => { + stdout = stdout + data.toString('utf8'); + }); + + await waitFor(() => stdout.includes('Enter an ID for the plugin')); + createPlugin.stdin.write('test-plugin\n'); + + await waitFor(() => stdout.includes('Enter the owner(s) of the plugin')); + createPlugin.stdin.write('@someuser\n'); + + print('Waiting for plugin create script to be done'); + await waitForExit(createPlugin); + + print('Test plugin created'); + } finally { + createPlugin.kill(); + } +} + +module.exports = createTestPlugin; diff --git a/scripts/helpers.js b/scripts/helpers.js new file mode 100644 index 0000000000..7ad1bedfd5 --- /dev/null +++ b/scripts/helpers.js @@ -0,0 +1,137 @@ +/* + * 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. + */ + +const childProcess = require('child_process'); +const { spawn } = childProcess; + +const EXPECTED_LOAD_ERRORS = /ECONNREFUSED|ECONNRESET|did not get to load all resources/; + +function spawnPiped(cmd, options) { + function pipeWithPrefix(stream, prefix = '') { + return data => { + const prefixedMsg = data + .toString('utf8') + .trimRight() + .replace(/^/gm, prefix); + stream.write(`${prefixedMsg}\n`, 'utf8'); + }; + } + + const child = spawn(cmd[0], cmd.slice(1), { + stdio: 'pipe', + shell: true, + ...options, + }); + child.on('error', handleError); + child.on('exit', code => { + if (code) { + print(`Child '${cmd.join(' ')}' exited with code ${code}`); + process.exit(code); + } + }); + child.stdout.on( + 'data', + pipeWithPrefix(process.stdout, `[${cmd.join(' ')}].out: `), + ); + child.stderr.on( + 'data', + pipeWithPrefix(process.stderr, `[${cmd.join(' ')}].err: `), + ); + + return child; +} + +function handleError(err) { + process.stdout.write(`${err.name}: ${err.stack || err.message}\n`); + if (typeof err.code === 'number') { + process.exit(err.code); + } else { + process.exit(1); + } +} + +function waitFor(fn) { + return new Promise(resolve => { + const handle = setInterval(() => { + if (fn()) { + clearInterval(handle); + resolve(); + return; + } + }, 100); + }); +} + +async function waitForExit(child) { + if (child.exitCode !== null) { + throw new Error(`Child already exited with code ${child.exitCode}`); + } + await new Promise((resolve, reject) => + child.once('exit', code => { + if (code) { + reject(new Error(`Child exited with code ${code}`)); + } else { + print('Child finished'); + resolve(); + } + }), + ); +} + +async function waitForPageWithText( + browser, + path, + text, + { intervalMs = 1000, maxAttempts = 240 } = {}, +) { + let attempts = 0; + for (;;) { + try { + await new Promise(resolve => setTimeout(resolve, intervalMs)); + await browser.visit(path); + break; + } catch (error) { + if (error.message.match(EXPECTED_LOAD_ERRORS)) { + attempts++; + if (attempts > maxAttempts) { + throw new Error( + `Failed to load page '${path}', max number of attempts reached`, + ); + } + } else { + throw error; + } + } + } + + const escapedText = text.replace(/"/g, '\\"'); + browser.assert.evaluate( + `Array.from(document.querySelectorAll("*")).some(el => el.textContent === "${escapedText}")`, + true, + `expected to find text ${text}`, + ); +} + +function print(msg) { + return process.stdout.write(`${msg}\n`); +} + +module.exports.spawnPiped = spawnPiped; +module.exports.handleError = handleError; +module.exports.waitFor = waitFor; +module.exports.waitForExit = waitForExit; +module.exports.waitForPageWithText = waitForPageWithText; +module.exports.print = print;