Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: setup e2e tests #264

Merged
merged 5 commits into from
Mar 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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