Skip to content

Commit

Permalink
chore: setup e2e tests (#264)
Browse files Browse the repository at this point in the history
* chore: setup e2e tests

* add flowfixmes

* use test.each

* add docs to install/uninstall

* remove dead code
  • Loading branch information
thymikee authored and grabbou committed Mar 28, 2019
1 parent 590ce4a commit 75ccf8e
Show file tree
Hide file tree
Showing 10 changed files with 848 additions and 20 deletions.
3 changes: 0 additions & 3 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,3 @@ unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import

[version]
^0.94.0
34 changes: 34 additions & 0 deletions e2e/__tests__/install.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @flow
import path from 'path';
import {run, getTempDirectory, cleanup, writeFiles} from '../helpers';

const DIR = getTempDirectory('command-install-test');
const pkg = 'react-native-config';

beforeEach(() => {
cleanup(DIR);
writeFiles(DIR, {
'node_modules/react-native/package.json': '{}',
'package.json': '{}',
});
});
afterEach(() => cleanup(DIR));

test.each(['yarn', 'npm'])('install module with %s', pm => {
if (pm === 'yarn') {
writeFiles(DIR, {'yarn.lock': ''});
}
const {stdout, code} = run(DIR, ['install', pkg]);

expect(stdout).toContain(`Installing "${pkg}"`);
expect(stdout).toContain(`Linking "${pkg}"`);
// TODO – this behavior is a bug, linking should fail/warn without native deps
// to link. Not a high priority since we're changing how link works
expect(stdout).toContain(`Successfully installed and linked "${pkg}"`);
expect(require(path.join(DIR, 'package.json'))).toMatchObject({
dependencies: {
[pkg]: expect.any(String),
},
});
expect(code).toBe(0);
});
55 changes: 55 additions & 0 deletions e2e/__tests__/uninstall.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// @flow
import {run, getTempDirectory, cleanup, writeFiles} from '../helpers';

const DIR = getTempDirectory('command-uninstall-test');
const pkg = 'react-native-config';

beforeEach(() => {
cleanup(DIR);
writeFiles(DIR, {
'node_modules/react-native/package.json': '{}',
'node_modules/react-native-config/package.json': '{}',
'package.json': `{
"dependencies": {
"react-native-config": "*"
}
}`,
});
});
afterEach(() => cleanup(DIR));

test('uninstall fails when package is not defined', () => {
writeFiles(DIR, {
'package.json': `{
"dependencies": {}
}`,
});
const {stderr, code} = run(DIR, ['uninstall']);

expect(stderr).toContain('missing required argument');
expect(code).toBe(1);
});

test('uninstall fails when package is not installed', () => {
writeFiles(DIR, {
'package.json': `{
"dependencies": {}
}`,
});
const {stderr, code} = run(DIR, ['uninstall', pkg]);

expect(stderr).toContain(`Project "${pkg}" is not a react-native library`);
expect(code).toBe(1);
});

test.each(['yarn', 'npm'])('uninstall module with %s', pm => {
if (pm === 'yarn') {
writeFiles(DIR, {'yarn.lock': ''});
}
const {stdout, code} = run(DIR, ['uninstall', pkg]);

expect(stdout).toContain(`Unlinking "${pkg}"`);
expect(stdout).toContain(`Uninstalling "${pkg}"`);
expect(stdout).toContain(`Successfully uninstalled and unlinked "${pkg}"`);
expect(code).toBe(0);
});
149 changes: 149 additions & 0 deletions e2e/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// @flow
import fs from 'fs';
import os from 'os';
import path from 'path';
import {createDirectory} from 'jest-util';
import rimraf from 'rimraf';
import execa from 'execa';
import {Writable} from 'readable-stream';

const CLI_PATH = path.resolve(__dirname, '../packages/cli/build/bin.js');

type RunOptions = {
nodeOptions?: string,
nodePath?: string,
timeout?: number, // kill the process after X milliseconds
};

export function run(
dir: string,
args?: Array<string>,
options: RunOptions = {},
) {
return spawnCli(dir, args, options);
}

// Runs cli until a given output is achieved, then kills it with `SIGTERM`
export async function runUntil(
dir: string,
args: Array<string> | void,
text: string,
options: RunOptions = {},
) {
const spawnPromise = spawnCliAsync(dir, args, {timeout: 30000, ...options});

spawnPromise.stderr.pipe(
new Writable({
write(chunk, _encoding, callback) {
const output = chunk.toString('utf8');

if (output.includes(text)) {
spawnPromise.kill();
}

callback();
},
}),
);

return spawnPromise;
}

export const makeTemplate = (
str: string,
): ((values?: Array<any>) => string) => (values?: Array<any>) =>
str.replace(/\$(\d+)/g, (_match, number) => {
if (!Array.isArray(values)) {
throw new Error('Array of values must be passed to the template.');
}
return values[number - 1];
});

export const cleanup = (directory: string) => rimraf.sync(directory);

/**
* Creates a nested directory with files and their contents
* writeFiles(
* '/home/tmp',
* {
* 'package.json': '{}',
* 'dir/file.js': 'module.exports = "x";',
* }
* );
*/
export const writeFiles = (
directory: string,
files: {[filename: string]: string},
) => {
createDirectory(directory);
Object.keys(files).forEach(fileOrPath => {
const dirname = path.dirname(fileOrPath);

if (dirname !== '/') {
createDirectory(path.join(directory, dirname));
}
fs.writeFileSync(
path.resolve(directory, ...fileOrPath.split('/')),
files[fileOrPath],
);
});
};

export const copyDir = (src: string, dest: string) => {
const srcStat = fs.lstatSync(src);
if (srcStat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest);
}
fs.readdirSync(src).map(filePath =>
copyDir(path.join(src, filePath), path.join(dest, filePath)),
);
} else {
fs.writeFileSync(dest, fs.readFileSync(src));
}
};

export const getTempDirectory = (name: string) =>
path.resolve(os.tmpdir(), name);

function spawnCli(dir: string, args?: Array<string>, options: RunOptions = {}) {
const {spawnArgs, spawnOptions} = getCliArguments({dir, args, options});

return execa.sync(process.execPath, spawnArgs, spawnOptions);
}

function spawnCliAsync(
dir: string,
args?: Array<string>,
options: RunOptions = {},
) {
const {spawnArgs, spawnOptions} = getCliArguments({dir, args, options});

return execa(process.execPath, spawnArgs, spawnOptions);
}

function getCliArguments({dir, args, options}) {
const isRelative = !path.isAbsolute(dir);

if (isRelative) {
dir = path.resolve(__dirname, dir);
}

const env = Object.assign({}, process.env, {FORCE_COLOR: '0'});

if (options.nodeOptions) {
env.NODE_OPTIONS = options.nodeOptions;
}
if (options.nodePath) {
env.NODE_PATH = options.nodePath;
}

const spawnArgs = [CLI_PATH, ...(args || [])];
const spawnOptions = {
cwd: dir,
env,
reject: false,
timeout: options.timeout || 0,
};
return {spawnArgs, spawnOptions};
}
4 changes: 4 additions & 0 deletions e2e/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/(?:.+?)/__tests__/'],
};
103 changes: 103 additions & 0 deletions flow-typed/npm/execa_v1.0.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// flow-typed signature: 613ee1ec7d728b6a312fcff21a7b2669
// flow-typed version: 3163f7a6e3/execa_v1.0.x/flow_>=v0.75.x

declare module 'execa' {

declare type StdIoOption =
| 'pipe'
| 'ipc'
| 'ignore'
| 'inherit'
| stream$Stream
| number;

declare type CommonOptions = {|
argv0?: string,
cleanup?: boolean,
cwd?: string,
detached?: boolean,
encoding?: string,
env?: {[string]: string},
extendEnv?: boolean,
gid?: number,
killSignal?: string | number,
localDir?: string,
maxBuffer?: number,
preferLocal?: boolean,
reject?: boolean,
shell?: boolean | string,
stderr?: ?StdIoOption,
stdin?: ?StdIoOption,
stdio?: 'pipe' | 'ignore' | 'inherit' | $ReadOnlyArray<?StdIoOption>,
stdout?: ?StdIoOption,
stripEof?: boolean,
timeout?: number,
uid?: number,
windowsVerbatimArguments?: boolean,
|};

declare type SyncOptions = {|
...CommonOptions,
input?: string | Buffer,
|};

declare type Options = {|
...CommonOptions,
input?: string | Buffer | stream$Readable,
|};

declare type SyncResult = {|
stdout: string,
stderr: string,
code: number,
failed: boolean,
signal: ?string,
cmd: string,
timedOut: boolean,
|};

declare type Result = {|
...SyncResult,
killed: boolean,
|};

declare interface ThenableChildProcess extends child_process$ChildProcess {
then<R, E>(
onfulfilled?: ?((value: Result) => R | Promise<R>),
onrejected?: ?((reason: ExecaError) => E | Promise<E>),
): Promise<R | E>;

catch<E>(
onrejected?: ?((reason: ExecaError) => E | Promise<E>)
): Promise<Result | E>;
}

declare interface ExecaError extends ErrnoError {
stdout: string;
stderr: string;
failed: boolean;
signal: ?string;
cmd: string;
timedOut: boolean;
}

declare interface Execa {
(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): ThenableChildProcess;
(file: string, options?: $ReadOnly<Options>): ThenableChildProcess;

stdout(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): Promise<string>;
stdout(file: string, options?: $ReadOnly<Options>): Promise<string>;

stderr(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): Promise<string>;
stderr(file: string, options?: $ReadOnly<Options>): Promise<string>;

shell(command: string, options?: $ReadOnly<Options>): ThenableChildProcess;

sync(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<SyncOptions>): SyncResult;
sync(file: string, options?: $ReadOnly<SyncOptions>): SyncResult;

shellSync(command: string, options?: $ReadOnly<Options>): SyncResult;
}

declare module.exports: Execa;
}
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"babel-jest": "^24.0.0",
"chalk": "^2.4.2",
"eslint": "^5.10.0",
"flow-bin": "^0.94.0",
"execa": "^1.0.0",
"flow-bin": "^0.95.1",
"flow-typed": "^2.5.1",
"glob": "^7.1.3",
"jest": "^24.0.0",
"lerna": "^3.10.6",
Expand All @@ -39,12 +41,16 @@
"node": true
},
"rules": {
"prettier/prettier": [2, "fb"]
"prettier/prettier": [
2,
"fb"
]
}
},
"jest": {
"projects": [
"packages/*"
"packages/*",
"e2e"
]
}
}
Loading

0 comments on commit 75ccf8e

Please sign in to comment.