Add e2e test case to test cli build options and webpack subroute handling.

Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
Aramis Sennyey
2022-11-29 12:55:29 -05:00
parent a271aa4c6c
commit cca591ee96
19 changed files with 598 additions and 6 deletions
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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 path = require('path');
module.exports = require('./config/jest').then(baseConfig => ({
...baseConfig,
rootDir: path.resolve(__dirname, 'e2e-tests'),
}));
+160
View File
@@ -0,0 +1,160 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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.
*/
import { execSync, spawn, SpawnOptionsWithoutStdio } from 'child_process';
import EventEmitter from 'events';
import mock from 'mock-fs';
import fetch from 'node-fetch';
import path from 'path';
const executeCommand = (
command: string,
args: string[],
options?: SpawnOptionsWithoutStdio,
events?: EventEmitter,
eventConfig?: {
killRegex?: RegExp;
signalRegex?: RegExp[];
},
): Promise<{
exit: number;
stdout: string;
stderr: string;
}> => {
return new Promise((resolve, reject) => {
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
const shell = process.platform === 'win32';
const proc = spawn(command, args, { ...options, shell });
proc.stdout?.on('data', data => {
stdout.push(Buffer.from(data));
});
proc.stderr?.on('data', data => {
stderr.push(Buffer.from(data));
});
let intervalId: NodeJS.Timer | undefined = undefined;
if (eventConfig) {
intervalId = setInterval(() => {
const stdoutStr = Buffer.concat(stdout).toString('utf8');
if (eventConfig.killRegex?.test(stdoutStr)) {
proc.kill('SIGINT');
} else if (
eventConfig.signalRegex?.some(regex => regex.test(stdoutStr))
) {
events?.emit('hit');
}
}, 1000);
}
events?.on('stop', signal => {
console.log(signal);
if (intervalId) {
try {
clearInterval(intervalId);
} catch (err) {
console.error(err);
}
}
proc.kill(signal);
});
proc.on('error', (...errorArgs) => {
if (intervalId) {
try {
clearInterval(intervalId);
} catch (err) {
console.error(err);
}
}
reject(errorArgs);
});
proc.on('exit', code => {
if (intervalId) {
try {
clearInterval(intervalId);
} catch (err) {
console.error(err);
}
}
resolve({
exit: code ?? 0,
stdout: Buffer.concat(stdout).toString('utf8'),
stderr: Buffer.concat(stderr).toString('utf8'),
});
});
});
};
const timeout = 40000;
jest.setTimeout(timeout * 2);
describe('end-to-end', () => {
const entryPoint = path.resolve(__dirname, '../bin/backstage-cli');
it.skip('shows help text', async () => {
const proc = await executeCommand(entryPoint, ['--help']);
expect(proc.stdout).toContain('Usage: backstage-cli [options]');
expect(proc.exit).toEqual(0);
});
it('builds frontend with correct url overrides', async () => {
const cwd = path.resolve(__dirname, 'test-project/packages/app');
const buildProc = await executeCommand(
entryPoint,
['package', 'build', '--public-path', '/test', '--backend-url', '/api'],
{
cwd,
},
);
expect(buildProc.stderr).toContain(
'Loaded config from app-config.yaml, cli',
);
console.log(buildProc.stderr, buildProc.stdout);
expect(buildProc.exit).toEqual(0);
});
it('starts frontend on correct url', async () => {
const cwd = path.resolve(__dirname, 'test-project/packages/app');
const startEmitter = new EventEmitter();
startEmitter.on('hit', async () => {
const response = await fetch('http://localhost:3000/test/catalog');
const text = await response.text();
startEmitter.emit('stop', 'SIGINT');
expect(response.status).toBe(200);
expect(text.length).toBeGreaterThan(0);
});
const startProc = await executeCommand(
entryPoint,
['package', 'start'],
{
cwd,
},
startEmitter,
{
signalRegex: [/webpack compiled/],
},
);
expect(startProc.exit).toEqual(0);
});
});
@@ -0,0 +1,6 @@
app:
baseUrl: http://localhost:3000/test
title: test
backend:
baseUrl: http://localhost:7007
@@ -0,0 +1,3 @@
{
"version": "1.7.0-next.0"
}
@@ -0,0 +1,54 @@
{
"name": "root",
"version": "1.0.0",
"private": true,
"engines": {
"node": "14 || 16"
},
"scripts": {
"dev": "concurrently \"yarn start\" \"yarn start-backend\"",
"start": "yarn workspace app start",
"start-backend": "yarn workspace backend start",
"build": "backstage-cli repo build --all",
"build-image": "yarn workspace backend build-image",
"tsc": "tsc",
"tsc:full": "tsc --skipLibCheck false --incremental false",
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"lint": "backstage-cli repo lint --since origin/master",
"lint:all": "backstage-cli repo lint",
"prettier:check": "prettier --check .",
"create-plugin": "backstage-cli create-plugin --scope internal",
"new": "backstage-cli new --scope internal"
},
"workspaces": {
"packages": [
"packages/*",
"plugins/*"
]
},
"devDependencies": {
"@backstage/cli": "^0.20.0-next.0",
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^6.0.0",
"lerna": "^4.0.0",
"node-gyp": "^9.0.0",
"prettier": "^2.3.2",
"typescript": "~4.6.4"
},
"resolutions": {
"@types/react": "^17",
"@types/react-dom": "^17"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
"*.{js,jsx,ts,tsx,mjs,cjs}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
@@ -0,0 +1,82 @@
{
"name": "app",
"version": "0.0.0",
"private": true,
"bundled": true,
"backstage": {
"role": "frontend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
"cy:run": "cypress run --browser chrome"
},
"dependencies": {
"@backstage/app-defaults": "^1.0.7-next.0",
"@backstage/catalog-model": "^1.1.2-next.0",
"@backstage/cli": "^0.20.0-next.0",
"@backstage/core-app-api": "^1.1.1-next.0",
"@backstage/core-components": "^0.11.2-next.0",
"@backstage/core-plugin-api": "^1.0.7-next.0",
"@backstage/integration-react": "^1.1.5-next.0",
"@backstage/plugin-api-docs": "^0.8.10-next.0",
"@backstage/plugin-catalog": "^1.5.2-next.0",
"@backstage/plugin-catalog-common": "^1.0.7-next.0",
"@backstage/plugin-catalog-graph": "^0.2.22-next.0",
"@backstage/plugin-catalog-import": "^0.8.13-next.0",
"@backstage/plugin-catalog-react": "^1.1.5-next.0",
"@backstage/plugin-github-actions": "^0.5.10-next.0",
"@backstage/plugin-org": "^0.5.10-next.0",
"@backstage/plugin-permission-react": "^0.4.6-next.0",
"@backstage/plugin-scaffolder": "^1.7.0-next.0",
"@backstage/plugin-search": "^1.0.3-next.0",
"@backstage/plugin-search-react": "^1.1.1-next.0",
"@backstage/plugin-tech-radar": "^0.5.17-next.0",
"@backstage/plugin-techdocs": "^1.3.3-next.0",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.5-next.0",
"@backstage/plugin-techdocs-react": "^1.0.5-next.0",
"@backstage/plugin-user-settings": "^0.5.0-next.0",
"@backstage/theme": "^0.2.16",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"history": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/test-utils": "^1.2.1-next.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"@types/react-dom": "*",
"cross-env": "^7.0.0",
"cypress": "^9.7.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"files": [
"dist"
]
}
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Backstage is an open platform for building developer portals"
/>
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link
rel="manifest"
href="<%= publicPath %>/manifest.json"
crossorigin="use-credentials"
/>
<link rel="icon" href="<%= publicPath %>/favicon.ico" />
<link rel="shortcut icon" href="<%= publicPath %>/favicon.ico" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="<%= publicPath %>/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="<%= publicPath %>/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="<%= publicPath %>/favicon-16x16.png"
/>
<link
rel="mask-icon"
href="<%= publicPath %>/safari-pinned-tab.svg"
color="#5bbad5"
/>
<title><%= config.getString('app.title') %></title>
<% if (config.has('app.googleAnalyticsTrackingId')) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= config.getString('app.googleAnalyticsTrackingId') %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag(
'config',
'<%= config.getString("app.googleAnalyticsTrackingId") %>',
);
</script>
<% } %>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,22 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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.
*/
import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<div />, document.getElementById('root'));
@@ -0,0 +1,51 @@
{
"name": "backend",
"version": "0.0.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"private": true,
"backstage": {
"role": "backend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/backend-common": "^0.15.0",
"@backstage/backend-tasks": "^0.3.4",
"@backstage/catalog-client": "^1.0.4",
"@backstage/catalog-model": "^1.1.0",
"@backstage/config": "^1.0.1",
"@backstage/plugin-app-backend": "^0.3.35",
"@backstage/plugin-auth-backend": "^0.15.1",
"@backstage/plugin-catalog-backend": "^1.3.1",
"@backstage/plugin-permission-common": "^0.6.3",
"@backstage/plugin-permission-node": "^0.6.4",
"@backstage/plugin-proxy-backend": "^0.2.29",
"@backstage/plugin-scaffolder-backend": "^1.5.0",
"@backstage/plugin-search-backend": "^1.0.1",
"@backstage/plugin-search-backend-module-pg": "^0.3.6",
"@backstage/plugin-search-backend-node": "^1.0.1",
"@backstage/plugin-techdocs-backend": "^1.2.1",
"dockerode": "^3.3.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"pg": "^8.3.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.18.1",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/luxon": "^2.0.4",
"better-sqlite3": "^7.5.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,15 @@
/*
* Copyright 2022 The Backstage Authors
*
* 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.
*/
+4 -1
View File
@@ -24,7 +24,9 @@
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"start": "nodemon --"
"start": "nodemon --",
"test:e2e": "backstage-cli test --config e2e-test.config.js",
"test:e2e:ci": "backstage-cli test --config e2e-test.config.js --watchAll=false --ci"
},
"bin": {
"backstage-cli": "bin/backstage-cli"
@@ -153,6 +155,7 @@
"@types/tar": "^6.1.1",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/yarnpkg__lockfile": "^1.1.4",
"cross-fetch": "^3.1.5",
"del": "^6.0.0",
"mock-fs": "^5.1.0",
"msw": "^0.49.0",
@@ -29,6 +29,7 @@ export async function command(opts: OptionValues): Promise<void> {
targetDir: paths.targetDir,
configPaths: opts.config as string[],
writeStats: Boolean(opts.stats),
cliOptions: opts,
});
}
if (role === 'backend') {
+8
View File
@@ -134,6 +134,14 @@ export function registerScriptCommand(program: Command) {
'--stats',
'If bundle stats are available, write them to the output directory. Applies to app packages only.',
)
.option(
'--public-path <path>',
'Public path for hosting the website on, can be relative.',
)
.option(
'--backend-url <url>',
'Backend url, expects just the origin or sub-route. Do not include /api. Can be relative.',
)
.option(
'--config <path>',
'Config files to load instead of app-config.yaml. Applies to app packages only.',
+1 -4
View File
@@ -153,10 +153,7 @@ export async function command(opts: OptionValues, cmd: Command): Promise<void> {
return;
}
await buildFrontend({
cliOptions: {
publicPath: opts.publicPath,
backendUrl: opts.backendUrl,
},
cliOptions: opts,
targetDir: pkg.dir,
configPaths: (buildOptions.config as string[]) ?? [],
writeStats: Boolean(buildOptions.stats),
+1 -1
View File
@@ -62,7 +62,7 @@ export async function serveBundle(options: ServeOptions) {
disableDotRule: true,
// The index needs to be rewritten relative to the new public path, including subroutes.
index: config.output?.publicPath ?? '/index.html',
index: `${config.output?.publicPath}index.html`,
},
https:
url.protocol === 'https:'
@@ -0,0 +1,86 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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.
*/
import { readCliConfig } from './cli';
describe('readCliConfig', () => {
it('should return empty config for empty cli', () => {
expect(readCliConfig({})).toEqual([]);
});
it('should return empty config for no matching keys', () => {
expect(
readCliConfig({
test: '123',
} as any),
).toEqual([]);
});
it('should return backend.baseUrl when backendUrl present in cli options', () => {
expect(
readCliConfig({
backendUrl: 'http://localhost:3000',
}),
).toEqual([
{
data: {
backend: {
baseUrl: 'http://localhost:3000',
},
},
context: 'cli',
},
]);
});
it('should return app.baseUrl when publicPath present in cli options', () => {
expect(
readCliConfig({
publicPath: 'http://localhost:3000',
}),
).toEqual([
{
data: {
app: {
baseUrl: 'http://localhost:3000',
},
},
context: 'cli',
},
]);
});
it('should return app.baseUrl and backend.baseUrl when publicPath and backendUrl present in cli options', () => {
expect(
readCliConfig({
publicPath: 'http://localhost:3000',
backendUrl: 'http://localhost:3000/api',
}),
).toEqual([
{
data: {
app: {
baseUrl: 'http://localhost:3000',
},
backend: {
baseUrl: 'http://localhost:3000/api',
},
},
context: 'cli',
},
]);
});
});
+2
View File
@@ -45,5 +45,7 @@ export function readCliConfig(opts?: CliConfigOptions): AppConfig[] {
};
}
if (Object.keys(data).length === 0) return [];
return [{ data, context: 'cli' }];
}
+1
View File
@@ -3653,6 +3653,7 @@ __metadata:
chalk: ^4.0.0
chokidar: ^3.3.1
commander: ^9.1.0
cross-fetch: ^3.1.5
css-loader: ^6.5.1
del: ^6.0.0
diff: ^5.0.0