Skip to content

Commit 75ccf8e

Browse files
thymikeegrabbou
authored andcommitted
chore: setup e2e tests (#264)
* chore: setup e2e tests * add flowfixmes * use test.each * add docs to install/uninstall * remove dead code
1 parent 590ce4a commit 75ccf8e

File tree

10 files changed

+848
-20
lines changed

10 files changed

+848
-20
lines changed

.flowconfig

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,3 @@ unclear-type
6363
unsafe-getters-setters
6464
untyped-import
6565
untyped-type-import
66-
67-
[version]
68-
^0.94.0

e2e/__tests__/install.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// @flow
2+
import path from 'path';
3+
import {run, getTempDirectory, cleanup, writeFiles} from '../helpers';
4+
5+
const DIR = getTempDirectory('command-install-test');
6+
const pkg = 'react-native-config';
7+
8+
beforeEach(() => {
9+
cleanup(DIR);
10+
writeFiles(DIR, {
11+
'node_modules/react-native/package.json': '{}',
12+
'package.json': '{}',
13+
});
14+
});
15+
afterEach(() => cleanup(DIR));
16+
17+
test.each(['yarn', 'npm'])('install module with %s', pm => {
18+
if (pm === 'yarn') {
19+
writeFiles(DIR, {'yarn.lock': ''});
20+
}
21+
const {stdout, code} = run(DIR, ['install', pkg]);
22+
23+
expect(stdout).toContain(`Installing "${pkg}"`);
24+
expect(stdout).toContain(`Linking "${pkg}"`);
25+
// TODO – this behavior is a bug, linking should fail/warn without native deps
26+
// to link. Not a high priority since we're changing how link works
27+
expect(stdout).toContain(`Successfully installed and linked "${pkg}"`);
28+
expect(require(path.join(DIR, 'package.json'))).toMatchObject({
29+
dependencies: {
30+
[pkg]: expect.any(String),
31+
},
32+
});
33+
expect(code).toBe(0);
34+
});

e2e/__tests__/uninstall.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// @flow
2+
import {run, getTempDirectory, cleanup, writeFiles} from '../helpers';
3+
4+
const DIR = getTempDirectory('command-uninstall-test');
5+
const pkg = 'react-native-config';
6+
7+
beforeEach(() => {
8+
cleanup(DIR);
9+
writeFiles(DIR, {
10+
'node_modules/react-native/package.json': '{}',
11+
'node_modules/react-native-config/package.json': '{}',
12+
'package.json': `{
13+
"dependencies": {
14+
"react-native-config": "*"
15+
}
16+
}`,
17+
});
18+
});
19+
afterEach(() => cleanup(DIR));
20+
21+
test('uninstall fails when package is not defined', () => {
22+
writeFiles(DIR, {
23+
'package.json': `{
24+
"dependencies": {}
25+
}`,
26+
});
27+
const {stderr, code} = run(DIR, ['uninstall']);
28+
29+
expect(stderr).toContain('missing required argument');
30+
expect(code).toBe(1);
31+
});
32+
33+
test('uninstall fails when package is not installed', () => {
34+
writeFiles(DIR, {
35+
'package.json': `{
36+
"dependencies": {}
37+
}`,
38+
});
39+
const {stderr, code} = run(DIR, ['uninstall', pkg]);
40+
41+
expect(stderr).toContain(`Project "${pkg}" is not a react-native library`);
42+
expect(code).toBe(1);
43+
});
44+
45+
test.each(['yarn', 'npm'])('uninstall module with %s', pm => {
46+
if (pm === 'yarn') {
47+
writeFiles(DIR, {'yarn.lock': ''});
48+
}
49+
const {stdout, code} = run(DIR, ['uninstall', pkg]);
50+
51+
expect(stdout).toContain(`Unlinking "${pkg}"`);
52+
expect(stdout).toContain(`Uninstalling "${pkg}"`);
53+
expect(stdout).toContain(`Successfully uninstalled and unlinked "${pkg}"`);
54+
expect(code).toBe(0);
55+
});

e2e/helpers.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// @flow
2+
import fs from 'fs';
3+
import os from 'os';
4+
import path from 'path';
5+
import {createDirectory} from 'jest-util';
6+
import rimraf from 'rimraf';
7+
import execa from 'execa';
8+
import {Writable} from 'readable-stream';
9+
10+
const CLI_PATH = path.resolve(__dirname, '../packages/cli/build/bin.js');
11+
12+
type RunOptions = {
13+
nodeOptions?: string,
14+
nodePath?: string,
15+
timeout?: number, // kill the process after X milliseconds
16+
};
17+
18+
export function run(
19+
dir: string,
20+
args?: Array<string>,
21+
options: RunOptions = {},
22+
) {
23+
return spawnCli(dir, args, options);
24+
}
25+
26+
// Runs cli until a given output is achieved, then kills it with `SIGTERM`
27+
export async function runUntil(
28+
dir: string,
29+
args: Array<string> | void,
30+
text: string,
31+
options: RunOptions = {},
32+
) {
33+
const spawnPromise = spawnCliAsync(dir, args, {timeout: 30000, ...options});
34+
35+
spawnPromise.stderr.pipe(
36+
new Writable({
37+
write(chunk, _encoding, callback) {
38+
const output = chunk.toString('utf8');
39+
40+
if (output.includes(text)) {
41+
spawnPromise.kill();
42+
}
43+
44+
callback();
45+
},
46+
}),
47+
);
48+
49+
return spawnPromise;
50+
}
51+
52+
export const makeTemplate = (
53+
str: string,
54+
): ((values?: Array<any>) => string) => (values?: Array<any>) =>
55+
str.replace(/\$(\d+)/g, (_match, number) => {
56+
if (!Array.isArray(values)) {
57+
throw new Error('Array of values must be passed to the template.');
58+
}
59+
return values[number - 1];
60+
});
61+
62+
export const cleanup = (directory: string) => rimraf.sync(directory);
63+
64+
/**
65+
* Creates a nested directory with files and their contents
66+
* writeFiles(
67+
* '/home/tmp',
68+
* {
69+
* 'package.json': '{}',
70+
* 'dir/file.js': 'module.exports = "x";',
71+
* }
72+
* );
73+
*/
74+
export const writeFiles = (
75+
directory: string,
76+
files: {[filename: string]: string},
77+
) => {
78+
createDirectory(directory);
79+
Object.keys(files).forEach(fileOrPath => {
80+
const dirname = path.dirname(fileOrPath);
81+
82+
if (dirname !== '/') {
83+
createDirectory(path.join(directory, dirname));
84+
}
85+
fs.writeFileSync(
86+
path.resolve(directory, ...fileOrPath.split('/')),
87+
files[fileOrPath],
88+
);
89+
});
90+
};
91+
92+
export const copyDir = (src: string, dest: string) => {
93+
const srcStat = fs.lstatSync(src);
94+
if (srcStat.isDirectory()) {
95+
if (!fs.existsSync(dest)) {
96+
fs.mkdirSync(dest);
97+
}
98+
fs.readdirSync(src).map(filePath =>
99+
copyDir(path.join(src, filePath), path.join(dest, filePath)),
100+
);
101+
} else {
102+
fs.writeFileSync(dest, fs.readFileSync(src));
103+
}
104+
};
105+
106+
export const getTempDirectory = (name: string) =>
107+
path.resolve(os.tmpdir(), name);
108+
109+
function spawnCli(dir: string, args?: Array<string>, options: RunOptions = {}) {
110+
const {spawnArgs, spawnOptions} = getCliArguments({dir, args, options});
111+
112+
return execa.sync(process.execPath, spawnArgs, spawnOptions);
113+
}
114+
115+
function spawnCliAsync(
116+
dir: string,
117+
args?: Array<string>,
118+
options: RunOptions = {},
119+
) {
120+
const {spawnArgs, spawnOptions} = getCliArguments({dir, args, options});
121+
122+
return execa(process.execPath, spawnArgs, spawnOptions);
123+
}
124+
125+
function getCliArguments({dir, args, options}) {
126+
const isRelative = !path.isAbsolute(dir);
127+
128+
if (isRelative) {
129+
dir = path.resolve(__dirname, dir);
130+
}
131+
132+
const env = Object.assign({}, process.env, {FORCE_COLOR: '0'});
133+
134+
if (options.nodeOptions) {
135+
env.NODE_OPTIONS = options.nodeOptions;
136+
}
137+
if (options.nodePath) {
138+
env.NODE_PATH = options.nodePath;
139+
}
140+
141+
const spawnArgs = [CLI_PATH, ...(args || [])];
142+
const spawnOptions = {
143+
cwd: dir,
144+
env,
145+
reject: false,
146+
timeout: options.timeout || 0,
147+
};
148+
return {spawnArgs, spawnOptions};
149+
}

e2e/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testPathIgnorePatterns: ['<rootDir>/(?:.+?)/__tests__/'],
4+
};

flow-typed/npm/execa_v1.0.x.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// flow-typed signature: 613ee1ec7d728b6a312fcff21a7b2669
2+
// flow-typed version: 3163f7a6e3/execa_v1.0.x/flow_>=v0.75.x
3+
4+
declare module 'execa' {
5+
6+
declare type StdIoOption =
7+
| 'pipe'
8+
| 'ipc'
9+
| 'ignore'
10+
| 'inherit'
11+
| stream$Stream
12+
| number;
13+
14+
declare type CommonOptions = {|
15+
argv0?: string,
16+
cleanup?: boolean,
17+
cwd?: string,
18+
detached?: boolean,
19+
encoding?: string,
20+
env?: {[string]: string},
21+
extendEnv?: boolean,
22+
gid?: number,
23+
killSignal?: string | number,
24+
localDir?: string,
25+
maxBuffer?: number,
26+
preferLocal?: boolean,
27+
reject?: boolean,
28+
shell?: boolean | string,
29+
stderr?: ?StdIoOption,
30+
stdin?: ?StdIoOption,
31+
stdio?: 'pipe' | 'ignore' | 'inherit' | $ReadOnlyArray<?StdIoOption>,
32+
stdout?: ?StdIoOption,
33+
stripEof?: boolean,
34+
timeout?: number,
35+
uid?: number,
36+
windowsVerbatimArguments?: boolean,
37+
|};
38+
39+
declare type SyncOptions = {|
40+
...CommonOptions,
41+
input?: string | Buffer,
42+
|};
43+
44+
declare type Options = {|
45+
...CommonOptions,
46+
input?: string | Buffer | stream$Readable,
47+
|};
48+
49+
declare type SyncResult = {|
50+
stdout: string,
51+
stderr: string,
52+
code: number,
53+
failed: boolean,
54+
signal: ?string,
55+
cmd: string,
56+
timedOut: boolean,
57+
|};
58+
59+
declare type Result = {|
60+
...SyncResult,
61+
killed: boolean,
62+
|};
63+
64+
declare interface ThenableChildProcess extends child_process$ChildProcess {
65+
then<R, E>(
66+
onfulfilled?: ?((value: Result) => R | Promise<R>),
67+
onrejected?: ?((reason: ExecaError) => E | Promise<E>),
68+
): Promise<R | E>;
69+
70+
catch<E>(
71+
onrejected?: ?((reason: ExecaError) => E | Promise<E>)
72+
): Promise<Result | E>;
73+
}
74+
75+
declare interface ExecaError extends ErrnoError {
76+
stdout: string;
77+
stderr: string;
78+
failed: boolean;
79+
signal: ?string;
80+
cmd: string;
81+
timedOut: boolean;
82+
}
83+
84+
declare interface Execa {
85+
(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): ThenableChildProcess;
86+
(file: string, options?: $ReadOnly<Options>): ThenableChildProcess;
87+
88+
stdout(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): Promise<string>;
89+
stdout(file: string, options?: $ReadOnly<Options>): Promise<string>;
90+
91+
stderr(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<Options>): Promise<string>;
92+
stderr(file: string, options?: $ReadOnly<Options>): Promise<string>;
93+
94+
shell(command: string, options?: $ReadOnly<Options>): ThenableChildProcess;
95+
96+
sync(file: string, args?: $ReadOnlyArray<string>, options?: $ReadOnly<SyncOptions>): SyncResult;
97+
sync(file: string, options?: $ReadOnly<SyncOptions>): SyncResult;
98+
99+
shellSync(command: string, options?: $ReadOnly<Options>): SyncResult;
100+
}
101+
102+
declare module.exports: Execa;
103+
}

package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"babel-jest": "^24.0.0",
2424
"chalk": "^2.4.2",
2525
"eslint": "^5.10.0",
26-
"flow-bin": "^0.94.0",
26+
"execa": "^1.0.0",
27+
"flow-bin": "^0.95.1",
28+
"flow-typed": "^2.5.1",
2729
"glob": "^7.1.3",
2830
"jest": "^24.0.0",
2931
"lerna": "^3.10.6",
@@ -39,12 +41,16 @@
3941
"node": true
4042
},
4143
"rules": {
42-
"prettier/prettier": [2, "fb"]
44+
"prettier/prettier": [
45+
2,
46+
"fb"
47+
]
4348
}
4449
},
4550
"jest": {
4651
"projects": [
47-
"packages/*"
52+
"packages/*",
53+
"e2e"
4854
]
4955
}
5056
}

0 commit comments

Comments
 (0)