Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
run: yarn tsc --noEmit

- name: Run Unit Tests
run: yarn test
run: yarn test --coverage
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ npm install -D react-native-owl

The config file - which unless specified in the cli should live in `./owl.config.json` - is used to descript how Owl should run your app and your tests. Below you can find all the options the can be specified.

| Name | Required | Description |
| ------------------ | -------- | ------------------------------------------------------------ |
| **ios config** | | |
| `ios.workspace` | true | Path to the `.xcworkspace` file of your react-native project |
| **android config** | | |
| - | | |
| Name | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| **ios config** | | |
| `ios.workspace` | true | Path to the `.xcworkspace` file of your react-native project |
| `ios.scheme` | true | The name of the scheme you would like to use for building the app |
| `ios.buildCommand` | false | Will override the `xcodebuild` command making the above options obselete. To be used when the default options are not suitable |
| **android config** | | |
| `android.buildCommand` | false | Will override the `assembleDebug` gradle command. Should build the apk |

## CLI

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"ajv": "^7.0.1",
"execa": "^5.0.0",
"yargs": "^16.2.0"
},
"devDependencies": {
Expand Down
116 changes: 116 additions & 0 deletions src/cli/build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import execa from 'execa';

import { buildAndroid, buildHandler, buildIOS } from './build';
import * as configHelpers from './config';
import { BuildRunOptions, Config } from './types';

describe('build.ts', () => {
describe('buildIOS', () => {
const execMock = jest.spyOn(execa, 'command').mockImplementation();

beforeEach(() => {
execMock.mockReset();
});

it('builds an iOS project with workspace/scheme', async () => {
const config: Config = {
ios: {
workspace: 'ios/RNDemo.xcworkspace',
scheme: 'RNDemo',
},
};

await buildIOS(config);

expect(execMock).toHaveBeenCalledTimes(1);
expect(
execMock
).toHaveBeenCalledWith(
`xcodebuild -workspace ios/RNDemo.xcworkspace -scheme RNDemo -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -quiet`,
{ stdio: 'inherit' }
);
});

it('builds an iOS project with a custom build command', async () => {
const config: Config = {
ios: {
buildCommand: "echo 'Hello World'",
},
};

await buildIOS(config);

expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(`echo 'Hello World'`, {
stdio: 'inherit',
});
});
});

describe('buildAndroid', () => {
const execMock = jest.spyOn(execa, 'command').mockImplementation();

beforeEach(() => {
execMock.mockReset();
});

it('builds an Android project with workspace/scheme', async () => {
const config: Config = {
android: {},
};

await buildAndroid(config);

expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(
`cd android/ && ./gradlew assembleDebug && cd -`,
{
stdio: 'inherit',
}
);
});

it('builds an Android project with a custom build command', async () => {
const config: Config = {
android: {
buildCommand: "echo 'Hello World'",
},
};

await buildAndroid(config);

expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(`echo 'Hello World'`, {
stdio: 'inherit',
});
});
});

describe('buildHandler', () => {
const args = {
platform: 'ios',
config: './owl.config.json',
} as BuildRunOptions;

const config: Config = {
ios: {
buildCommand: "echo 'Hello World'",
},
android: {
buildCommand: "echo 'Hello World'",
},
};

it('builds an iOS project', async () => {
jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(config);
const call = async () => buildHandler(args);
await expect(call()).resolves.not.toThrow();
});

it('builds an Android project', async () => {
jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(config);
const call = async () => buildHandler({ ...args, platform: 'android' });
await expect(call()).resolves.not.toThrow();
});
});
});
22 changes: 20 additions & 2 deletions src/cli/build.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import execa from 'execa';

import { getConfig } from './config';
import { BuildRunOptions } from './types';
import { BuildRunOptions, Config } from './types';

export const buildIOS = async (config: Config): Promise<void> => {
const buildCommand =
config.ios?.buildCommand ||
`xcodebuild -workspace ${config.ios?.workspace} -scheme ${config.ios?.scheme} -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -quiet`;

await execa.command(buildCommand, { stdio: 'inherit' });
};

export const buildAndroid = async (config: Config): Promise<void> => {
const buildCommand =
config.android?.buildCommand ||
`cd android/ && ./gradlew assembleDebug && cd -`;
await execa.command(buildCommand, { stdio: 'inherit' });
};

export const buildHandler = async (args: BuildRunOptions) => {
const config = await getConfig(args.config);
const buildProject = args.platform === 'ios' ? buildIOS : buildAndroid;

console.log('Configuration:', JSON.stringify(config, null, 2));
await buildProject(config);

console.log(
`OWL will build for the ${args.platform} platform. Config file: ${args.config}`
Expand Down
110 changes: 110 additions & 0 deletions src/cli/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { promises as fs } from 'fs';

import { getConfig, readConfigFile, validateSchema } from './config';

describe('config.ts', () => {
describe('validateSchema', () => {
it('validates a config', async () => {
const config = {
ios: {
buildCommand: 'echo "Hello iOS"',
},
android: {
buildCommand: 'echo "Hello Android"',
},
};

const validate = async () => await validateSchema(config);

await expect(validate()).resolves.toEqual(config);
});

it('accepts an ios config that has workspace/scheme but not a buildCommand', async () => {
const config = { ios: { workspace: 'Test', scheme: 'Test' } };

const validate = async () => await validateSchema(config);

await expect(validate()).resolves.toEqual(config);
});

it('accepts an ios config that has buildCommand but not workspace/scheme', async () => {
const config = { ios: { workspace: 'Test', scheme: 'Test' } };

const validate = async () => await validateSchema(config);

await expect(validate()).resolves.toEqual(config);
});

it("rejects an ios config that doesn't have either workspace/scheme or buildCommand", async () => {
const config = { ios: {} };

const validate = async () => await validateSchema(config);

await expect(validate()).rejects.toContain(
"should have required property 'workspace'"
);
await expect(validate()).rejects.toContain(
"should have required property 'workspace'"
);
await expect(validate()).rejects.toContain(
'should match some schema in anyOf'
);
});

it('rejects a config that does not have either ios or android options', async () => {
const config = {};

const validate = async () => await validateSchema(config);

await expect(validate()).rejects.toContain(
"should have required property 'ios'"
);
await expect(validate()).rejects.toContain(
"should have required property 'android'"
);
});
});

describe('readConfigFile', () => {
it('reads a config file and returns JSON', async () => {
const buffer = Buffer.from(JSON.stringify({ hello: 'world' }), 'utf8');
jest.spyOn(fs, 'readFile').mockImplementationOnce(async () => buffer);

const filePath = './my-config.json';
const result = await readConfigFile(filePath);

expect(result.hello).toBe('world');
});

it('reads a config file - invalid file', async () => {
const filePath = './my-config.json';

const call = async () => await readConfigFile(filePath);

await expect(call()).rejects.toThrow(
`Could not load the config at ${filePath}`
);
});
});

describe('getConfig', () => {
it('returns a validated config', async () => {
const expectedConfig = {
ios: {
workspace: 'ios/RNDemo.xcworkspace',
scheme: 'RNDemo',
},
android: {},
};

const filePath = './owl.config.json';

const buffer = Buffer.from(JSON.stringify(expectedConfig), 'utf8');
jest.spyOn(fs, 'readFile').mockImplementationOnce(async () => buffer);

const result = await getConfig(filePath);

expect(result).toEqual(expectedConfig);
});
});
});
22 changes: 15 additions & 7 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@ import Ajv, { ErrorObject, JSONSchemaType } from 'ajv';

import { Config } from './types';

const validateSchema = (config: {}): Promise<Config> => {
const configSschema: JSONSchemaType<Config> = {
export const validateSchema = (config: {}): Promise<Config> => {
const configSchema: JSONSchemaType<Config> = {
type: 'object',
properties: {
ios: {
type: 'object',
properties: {
workspace: { type: 'string' },
workspace: { type: 'string', nullable: true },
scheme: { type: 'string', nullable: true },
buildCommand: { type: 'string', nullable: true },
},
required: ['workspace'],
required: [],
anyOf: [
{ required: ['workspace', 'scheme'] },
{ required: ['buildCommand'] },
],
nullable: true,
additionalProperties: false,
},
android: {
type: 'object',
properties: {},
properties: {
buildCommand: { type: 'string', nullable: true },
},
required: [],
nullable: true,
additionalProperties: false,
Expand All @@ -30,7 +38,7 @@ const validateSchema = (config: {}): Promise<Config> => {
};

const ajv = new Ajv();
const validate = ajv.compile(configSschema);
const validate = ajv.compile(configSchema);

return new Promise((resolve, reject) => {
if (validate(config)) {
Expand All @@ -44,7 +52,7 @@ const validateSchema = (config: {}): Promise<Config> => {
});
};

const readConfigFile = async (configPath: string) => {
export const readConfigFile = async (configPath: string) => {
try {
const configData = await fs.readFile(configPath, 'binary');
const configString = Buffer.from(configData).toString();
Expand Down
8 changes: 6 additions & 2 deletions src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ export interface BuildRunOptions extends Arguments {
}

type ConfigIOS = {
workspace: string;
workspace?: string;
scheme?: string;
buildCommand?: string;
};

type ConfigAndroid = {};
type ConfigAndroid = {
buildCommand?: string;
};

export type Config = {
ios?: ConfigIOS;
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
Expand Down
Loading