Initial attempt for e2e tests

- Add node code for e2e tests. This is a first take to try running
  these tests via actions.
- Relates to #401
This commit is contained in:
Janne Valkealahti
2022-05-06 13:41:10 +01:00
parent 8a23518b84
commit 3743a7f32d
17 changed files with 6844 additions and 0 deletions

51
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: e2e
on:
workflow_dispatch:
jobs:
e2e:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- nickname: win
os: windows-2019
graal: 22.0.0.2
- nickname: macos
os: macos-latest
graal: 22.0.0.2
- nickname: linux
os: ubuntu-latest
graal: 22.0.0.2
name: CI Native ${{ matrix.nickname }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: adopt
java-version: 11
cache: maven
- uses: graalvm/setup-graalvm@v1
with:
version: ${{ matrix.graal }}
java-version: 11
components: native-image
set-java-home: false
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: |
./mvnw clean package -Pnative
- name: compile e2e module
working-directory: e2e/spring-shell-e2e
run: |
npm install
npm run build
- name: run e2e tests
working-directory: e2e/spring-shell-e2e-tests
run: |
npm install
npm test

1
e2e/spring-shell-e2e-tests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,5 @@
# dont care about typings
types/
# for test data
test/data/

View File

@@ -0,0 +1,11 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"parser": "typescript"
}

View File

@@ -0,0 +1,12 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
setupFilesAfterEnv: ['jest-extended', '@alex_neo/jest-expect-message']
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "spring-shell-e2e-tests",
"version": "2.1.0",
"description": "e2e tests for spring-shell",
"main": "index.js",
"scripts": {
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts",
"test": "jest"
},
"repository": {
"type": "git",
"url": "git+https://github.com/spring-projects/spring-shell.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/spring-projects/spring-shell/issues"
},
"homepage": "https://github.com/spring-projects/spring-shell",
"devDependencies": {
"@actions/io": "^1.1.2",
"@alex_neo/jest-expect-message": "^1.0.5",
"@types/jest": "^27.4.1",
"@types/jest-expect-message": "^1.0.3",
"@types/lodash": "^4.14.172",
"@types/node": "^15.6.0",
"jest": "^27.5.1",
"jest-circus": "^27.5.1",
"jest-extended": "^2.0.0",
"prettier": "^2.3.0",
"ts-jest": "^27.1.3",
"typescript": "^4.6.2",
"wait-for-expect": "^3.0.2"
},
"dependencies": {
"spring-shell-e2e": "file:../spring-shell-e2e"
}
}

View File

@@ -0,0 +1,18 @@
import * as os from 'os';
import * as path from 'path';
export const tempDir = path.join(__dirname, 'spring-shell', 'temp');
export const isWindows = os.platform() === 'win32';
export const cliPathRelative = isWindows
? '..\\..\\spring-shell-samples\\target\\spring-shell-samples.exe'
: '../../spring-shell-samples/target/spring-shell-samples';
export const jarPathRelative = isWindows
? '..\\..\\spring-shell-samples\\target\\spring-shell-samples-2.1.0-SNAPSHOT-exec.jar'
: '../../spring-shell-samples/target/spring-shell-samples-2.1.0-SNAPSHOT-exec.jar';
export const cliPath = path.resolve(cliPathRelative);
export const jarPath = path.resolve(jarPathRelative);
export const nativeDesc = 'native';
export const jarDesc = 'jar';
export const jarCommand = 'java';
export const nativeCommand = cliPath;
export const jarOptions = ['-jar', jarPath];

View File

@@ -0,0 +1,69 @@
import 'jest-extended';
import waitForExpect from 'wait-for-expect';
import { Cli } from 'spring-shell-e2e';
import { nativeDesc, jarDesc, jarCommand, nativeCommand, jarOptions } from '../src/utils';
// all buildin commands
describe('builtin commands', () => {
let cli: Cli;
let command: string;
let options: string[] = [];
/**
* test for version command returns expected info
*/
const versionReturnsInfoDesc = 'version returns info';
const versionCommand = ['version'];
const versionReturnsInfo = async (cli: Cli) => {
cli.run();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Build Version')]));
});
await expect(cli.exitCode()).resolves.toBe(0);
};
beforeEach(async () => {
waitForExpect.defaults.timeout = 3000;
waitForExpect.defaults.interval = 100;
}, 300000);
afterEach(async () => {
cli?.dispose();
}, 100000);
/**
* native commands
*/
describe(nativeDesc, () => {
beforeAll(() => {
command = nativeCommand;
});
it(versionReturnsInfoDesc, async () => {
cli = new Cli({
command: command,
options: [...options, ...versionCommand]
});
await versionReturnsInfo(cli);
});
});
/**
* fatjar commands
*/
describe(jarDesc, () => {
beforeAll(() => {
command = jarCommand;
options = jarOptions;
});
it(versionReturnsInfoDesc, async () => {
cli = new Cli({
command: command,
options: [...options, ...versionCommand]
});
await versionReturnsInfo(cli);
});
});
});

View File

@@ -0,0 +1,91 @@
import 'jest-extended';
import waitForExpect from 'wait-for-expect';
import { Cli } from 'spring-shell-e2e';
import { nativeDesc, jarDesc, jarCommand, nativeCommand, jarOptions } from '../src/utils';
// all flow commands
describe('flow commands', () => {
let cli: Cli;
let command: string;
let options: string[] = [];
/**
* test for flow conditional field2 skips field1
*/
const flowConditionalField2SkipsFields1Desc = 'flow conditional field2 skips field1';
const flowConditionalCommand = ['flow', 'conditional'];
const flowConditionalField2SkipsFields1 = async (cli: Cli) => {
cli.run();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Single1')]));
});
await cli.keyDown();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('> Field2')]));
});
await cli.keyEnter();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(
expect.arrayContaining([expect.stringContaining('? Field2 [Default defaultField2Value]')])
);
});
await cli.keyEnter();
await waitForExpect(async () => {
const screen = cli.screen();
expect(screen).toEqual(expect.arrayContaining([expect.stringContaining('Field2 defaultField2Value')]));
});
await expect(cli.exitCode()).resolves.toBe(0);
};
beforeEach(async () => {
waitForExpect.defaults.timeout = 3000;
waitForExpect.defaults.interval = 100;
}, 300000);
afterEach(async () => {
cli?.dispose();
}, 100000);
/**
* native commands
*/
describe(nativeDesc, () => {
beforeAll(() => {
command = nativeCommand;
});
it(flowConditionalField2SkipsFields1Desc, async () => {
cli = new Cli({
command: command,
options: [...options, ...flowConditionalCommand]
});
await flowConditionalField2SkipsFields1(cli);
});
});
/**
* fatjar commands
*/
describe(jarDesc, () => {
beforeAll(() => {
command = jarCommand;
options = jarOptions;
});
it(flowConditionalField2SkipsFields1Desc, async () => {
cli = new Cli({
command: command,
options: [...options, ...flowConditionalCommand]
});
await flowConditionalField2SkipsFields1(cli);
});
});
});

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"outDir": "./lib",
"rootDirs": ["./src", "./test"],
"strict": true,
"baseUrl": ".",
"noImplicitAny": false,
"paths": {
"*": ["./types/*"]
},
"esModuleInterop": true,
"lib": [
"es2019"
]
},
"exclude": ["node_modules", "**/*.test.ts"]
}

2
e2e/spring-shell-e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
lib

View File

@@ -0,0 +1,11 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"parser": "typescript"
}

38
e2e/spring-shell-e2e/package-lock.json generated Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "spring-shell-e2e",
"version": "2.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
},
"node-pty": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.10.1.tgz",
"integrity": "sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg==",
"requires": {
"nan": "^2.14.0"
}
},
"prettier": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz",
"integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==",
"dev": true
},
"typescript": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz",
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
"dev": true
},
"xterm-headless": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/xterm-headless/-/xterm-headless-4.18.0.tgz",
"integrity": "sha512-VwSPG2cyVOwesVVLBVrybYNY6cUMiVkD2fLnIXcBs/1iJ3M7bhD3lP9HdQOHGtOkbxV2Duf7MZNmEfngBXL/iQ=="
}
}
}

View File

@@ -0,0 +1,41 @@
{
"name": "spring-shell-e2e",
"version": "2.1.0",
"description": "spring-shell e2e test framework",
"main": "./lib/cli.js",
"exports": {
".": "./lib/cli.js"
},
"typesVersions": {
"*": {
"cli.d.ts": [
"lib/cli.d.ts"
]
}
},
"type": "module",
"scripts": {
"build": "tsc",
"test": "jest",
"format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.ts"
},
"repository": {
"type": "git",
"url": "git+https://github.com/spring-projects/spring-shell.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/spring-projects/spring-shell/issues"
},
"homepage": "https://github.com/spring-projects/spring-shell#readme",
"devDependencies": {
"prettier": "^2.6.2",
"typescript": "^4.6.4"
},
"dependencies": {
"node-pty": "^0.10.1",
"xterm-headless": "^4.18.0"
}
}

View File

@@ -0,0 +1,128 @@
import * as pty from 'node-pty';
import { Terminal } from 'xterm-headless';
export interface CliOptions {
command: string;
options?: string[];
keyWait?: number;
cols?: number;
rows?: number;
}
export class Cli {
private isDisposed: boolean = false;
private pty: pty.IPty | undefined;
private term: Terminal | undefined;
private keyWait: number = 500;
private cols: number = 80;
private rows: number = 20;
private exit: Promise<number> | undefined;
constructor(private options: CliOptions) {
if (options.keyWait) {
this.keyWait = options.keyWait;
}
if (options.cols) {
this.cols = options.cols;
}
if (options.rows) {
this.rows = options.rows;
}
}
public run(): void {
this.pty = pty.spawn(this.options.command, this.options.options || [], {
name: 'xterm-256color',
cols: this.cols,
rows: this.rows
});
this.term = new Terminal({
cols: this.cols,
rows: this.rows
});
this.pty.onData(data => {
this.term?.write(data);
});
this.exit = new Promise(resolve => {
this.pty?.onExit(data => {
resolve(data.exitCode);
});
});
}
public exitCode(): Promise<number> {
if (!this.exit) {
return Promise.reject('cli has not been started');
}
return this.exit;
}
public screen(): string[] {
const l = this.term?.buffer.active.length || 0;
const ret: string[] = [];
for (let index = 0; index < l; index++) {
const line = this.term?.buffer.active.getLine(index)?.translateToString();
if (line) {
ret.push(line);
}
}
return ret;
}
public async keyText(data: string, wait: number): Promise<Cli> {
if (!this.pty) {
return Promise.reject('cli has not been started');
}
this.pty.write(data);
await this.doWait(wait);
return this;
}
public async keyUp(wait?: number): Promise<Cli> {
if (!this.pty) {
return Promise.reject('cli has not been started');
}
this.pty.write('\x1BOA');
await this.doWait(wait);
return this;
}
public async keyDown(wait?: number): Promise<Cli> {
if (!this.pty) {
return Promise.reject('cli has not been started');
}
this.pty.write('\x1BOB');
await this.doWait(wait);
return this;
}
public async keyEnter(wait?: number): Promise<Cli> {
if (!this.pty) {
return Promise.reject('cli has not been started');
}
this.pty.write('\x0D');
await this.doWait(wait);
return this;
}
public dispose(): void {
if (this.isDisposed) {
return;
}
if (this.pty) {
this.pty.kill();
}
if (this.term) {
this.term.dispose();
}
this.isDisposed = true;
}
private async doWait(wait?: number): Promise<void> {
await this.sleep(wait || this.keyWait);
}
private async sleep(ms: number): Promise<void> {
return new Promise(r => setTimeout(r, ms));
}
}

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"outDir": "./lib", /* Specify an output folder for all emitted files. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}