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
4 changes: 2 additions & 2 deletions .github/workflows/demo-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ jobs:
run: yarn owl:test:ios
working-directory: ./example

- name: Store screenshots as artifacts
- name: Store screenshots and report as artifacts
uses: actions/upload-artifact@v2
if: failure()
with:
name: owl-screenshots
name: owl-results
path: example/.owl
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,22 @@ The config file - which unless specified in the cli should live in `./owl.config

### Options

| Name | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------------------- |
| **general** | | |
| `debug` | false | Prevents the CLI/library from printing any logs/output |
| **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.configuration` | true | The build configuration that should be used. Defaults to Debug |
| `ios.buildCommand` | false | Overrides the `xcodebuild` command making the above options obselete |
| `ios.binaryPath` | false | The path to the binary, if you are using a custom build command |
| `ios.quiet` | false | Passes the quiet flag to `xcode builds` |
| **android config** | | |
| `android.buildCommand` | false | Overrides the `assembleDebug` gradle command. Should build the apk |
| `android.binaryPath` | false | The path to the binary, if you are using a custom build command |
| `android.quiet` | false | Passes the quiet flag to `gradlew` |
| Name | Required | Default | Description |
| ---------------------- | -------- | ------- | ----------------------------------------------------------------------- |
| **general** | | | |
| `debug` | false | `false` | Prevents the CLI/library from printing any logs/output. |
| `report` | false | `true` | Generate an HTML report, displaying the baseline, latest & diff images. |
| **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.configuration` | true | `Debug` | The build configuration that should be used. |
| `ios.buildCommand` | false | | Overrides the `xcodebuild` command making the above options obselete |
| `ios.binaryPath` | false | | The path to the binary, if you are using a custom build command |
| `ios.quiet` | false | | Passes the quiet flag to `xcode builds` |
| **android config** | | | |
| `android.buildCommand` | false | | Overrides the `assembleDebug` gradle command. Should build the apk |
| `android.binaryPath` | false | | The path to the binary, if you are using a custom build command |
| `android.quiet` | false | | Passes the quiet flag to `gradlew` |

### Example

Expand Down
1 change: 1 addition & 0 deletions example/.owl/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
diff/
latest/
report/
3 changes: 2 additions & 1 deletion example/owl.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"packageName": "com.owldemo",
"quiet": true
},
"debug": true
"debug": true,
"report": true
}
2 changes: 2 additions & 0 deletions lib/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ describe('config.ts', () => {
android: {
packageName: 'com.rndemo',
},
debug: false,
report: true,
};

const filePath = './owl.config.json';
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ export const validateSchema = (config: {}): Promise<Config> => {
nullable: true,
additionalProperties: false,
},
debug: { type: 'boolean', nullable: true },
debug: { type: 'boolean', nullable: true, default: false },
report: { type: 'boolean', nullable: true, default: true },
},
required: [],
anyOf: [{ required: ['ios'] }, { required: ['android'] }],
Expand Down
61 changes: 60 additions & 1 deletion lib/cli/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CliRunOptions, Config } from '../types';
import { Logger } from '../logger';
import * as configHelpers from './config';
import * as run from './run';
import * as reportHelpers from '../report';

describe('run.ts', () => {
const logger = new Logger();
Expand Down Expand Up @@ -202,11 +203,13 @@ describe('run.ts', () => {
)} --roots=${path.join(process.cwd())} --runInBand`;

const commandSyncMock = jest.spyOn(execa, 'commandSync');
const mockGenerateReport = jest.spyOn(reportHelpers, 'generateReport');

jest.spyOn(Logger.prototype, 'print').mockImplementation();

beforeEach(() => {
commandSyncMock.mockReset();
mockGenerateReport.mockReset();
});

it('runs an iOS project', async () => {
Expand Down Expand Up @@ -247,7 +250,7 @@ describe('run.ts', () => {
});
});

it('runs with the the update baseline flag on', async () => {
it('runs with the update baseline flag on', async () => {
jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(config);
const mockRunIOS = jest.spyOn(run, 'runIOS').mockResolvedValueOnce();

Expand All @@ -264,5 +267,61 @@ describe('run.ts', () => {
stdio: 'inherit',
});
});

it('runs generates the report if the config is set to on', async () => {
const caseConfig: Config = {
...config,
report: true,
};

jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(caseConfig);
const mockRunIOS = jest.spyOn(run, 'runIOS').mockResolvedValueOnce();

commandSyncMock.mockRejectedValueOnce(undefined!);

try {
await run.runHandler({ ...args, update: true });
} catch {
await expect(mockRunIOS).toHaveBeenCalled();
await expect(commandSyncMock).toHaveBeenCalledTimes(1);
await expect(mockGenerateReport).toHaveBeenCalledTimes(1);
}
});

it('does not generate the report if the config is set to off', async () => {
const caseConfig: Config = {
...config,
report: false,
};

jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(caseConfig);
const mockRunIOS = jest.spyOn(run, 'runIOS').mockResolvedValueOnce();

commandSyncMock.mockRejectedValueOnce(undefined!);

try {
await run.runHandler({ ...args, update: true });
} catch {
await expect(mockRunIOS).toHaveBeenCalled();
await expect(commandSyncMock).toHaveBeenCalledTimes(1);
await expect(mockGenerateReport).not.toHaveBeenCalled();
}
});

it('does not generate the report if the tests pass', async () => {
const caseConfig: Config = {
...config,
report: true,
};

jest.spyOn(configHelpers, 'getConfig').mockResolvedValueOnce(caseConfig);
const mockRunIOS = jest.spyOn(run, 'runIOS').mockResolvedValueOnce();

await run.runHandler({ ...args, update: true });

await expect(mockRunIOS).toHaveBeenCalled();
await expect(commandSyncMock).toHaveBeenCalledTimes(1);
await expect(mockGenerateReport).not.toHaveBeenCalled();
});
});
});
25 changes: 17 additions & 8 deletions lib/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import execa from 'execa';

import { CliRunOptions, Config } from '../types';
import { generateReport } from '../report';
import { getConfig } from './config';
import { Logger } from '../logger';

Expand Down Expand Up @@ -87,14 +88,22 @@ export const runHandler = async (args: CliRunOptions) => {
logger.info(`[OWL] Will use the jest config localed at ${jestConfigPath}.`);
logger.info(`[OWL] Will set the jest root to ${process.cwd()}.`);

await execa.commandSync(jestCommand, {
stdio: 'inherit',
env: {
OWL_PLATFORM: args.platform,
OWL_DEBUG: String(!!config.debug),
OWL_UPDATE_BASELINE: String(!!args.update),
},
});
try {
await execa.commandSync(jestCommand, {
stdio: 'inherit',
env: {
OWL_PLATFORM: args.platform,
OWL_DEBUG: String(!!config.debug),
OWL_UPDATE_BASELINE: String(!!args.update),
},
});
} catch (err) {
if (config.report) {
await generateReport(logger, args.platform);
}

throw err;
}

logger.print(`[OWL] Tests completed on ${args.platform}.`);
if (args.update) {
Expand Down
2 changes: 2 additions & 0 deletions lib/matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('matchers.ts', () => {
const imageHello2Data = `iVBORw0KGgoAAAANSUhEUgAAACUAAAALCAYAAAD4OERFAAAABGdBTUEAALGPC/xhBQAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAJaADAAQAAAABAAAACwAAAADN8bJQAAABsklEQVQ4Ec2UOyhFcRzHj1d5lEEhogyKQqEMyioDxYJBCoMMsngslDvYDQyUYlJsHimURZJQSi6RV15ZxGD0+Hxv///pdDq3bnfhW5/7e/3/5/4fv3Mcx3ESIAX+lWpZzWkcK7pmTiVUw1Uc83uYswZL0ALuwSQT/IXa+dNxgzY3DbkwC06ifpCuMAR3cATlYNWLEzYM2GQUq+dNwDM8wRjo2X7Vk5iEBdiFOWgDV7q+b5iCCliEFZCa4RYaQLVz6AAp6Pr6yR9CIZSAxneBX/kkMjzJZfyQJ3a0qHewp1aHf2kGbGGHjC/TB3bBQYs6od6tgUZa5KYNAmw6uRm4B20kIttTOm6dlvQJaRHPcYqxNTBiYjXjhfGDTBHJA0/hBj/bE3vdMoJ1ULtUwRtEZE/nyyZ89oNYu80x5GEbIZrOKBR4irrCfU9sXW12FYahFdwF4btXJj9IOyT1pqQa5rGDEE3aeSdovBq8CbbBr1ES+hRsgK5QaI4r9ZT3O6WjfDDVLOwevMArqMcyQQrqKZ1SGB5B16xF2lbAdaXaj49jtxqDox3ruEUsSmJQKegNi0u/XtRShUjycDoAAAAASUVORK5CYII=`;
const imageHello2Buffer = Buffer.from(imageHello2Data, 'base64');

const mkdirSyncMock = jest.spyOn(fs, 'mkdirSync').mockImplementation();
const readFileMock = jest.spyOn(fs, 'readFileSync');
const writeFileMock = jest.spyOn(fs, 'writeFileSync');

Expand All @@ -28,6 +29,7 @@ describe('matchers.ts', () => {

describe('toMatchBaseline.ts', () => {
beforeEach(() => {
mkdirSyncMock.mockReset();
readFileMock.mockReset();
writeFileMock.mockReset();
});
Expand Down
54 changes: 54 additions & 0 deletions lib/report.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import process from 'process';
import handlebars from 'handlebars';
import { promises as fs } from 'fs';

import { Logger } from './logger';
import { generateReport } from './report';

describe('report.ts', () => {
const logger = new Logger();

const htmlTemplate = '<h1>Hello World<h1>';

const readdirMock = jest.spyOn(fs, 'readdir');
const mkdirMock = jest.spyOn(fs, 'mkdir');

const readFileMock = jest.spyOn(fs, 'readFile');
const writeFileMock = jest.spyOn(fs, 'writeFile');

const handlebarsCompileMock = jest
.spyOn(handlebars, 'compile')
.mockImplementationOnce(() => () => '<h1>Hello World Compiled</h1>');

const cwdMock = jest
.spyOn(process, 'cwd')
.mockReturnValue('/Users/johndoe/Projects/my-project');

beforeAll(() => {
readdirMock.mockReset();
mkdirMock.mockReset();

readFileMock.mockReset();
writeFileMock.mockReset();
});

afterAll(() => {
cwdMock.mockRestore();
});

it('should get the screenshots and create the html report', async () => {
readFileMock.mockResolvedValueOnce(htmlTemplate);
mkdirMock.mockResolvedValueOnce(undefined);

await generateReport(logger, 'ios');

expect(readdirMock).toHaveBeenCalledWith(
'/Users/johndoe/Projects/my-project/.owl/latest/ios'
);
expect(handlebarsCompileMock).toHaveBeenCalledTimes(1);
expect(writeFileMock).toHaveBeenCalledWith(
'/Users/johndoe/Projects/my-project/.owl/report/index.html',
'<h1>Hello World Compiled</h1>'
);
});
});
32 changes: 32 additions & 0 deletions lib/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'path';
import handlebars from 'handlebars';
import { promises as fs } from 'fs';

import { Logger } from './logger';
import { Platform } from './types';

export const generateReport = async (logger: Logger, platform: Platform) => {
const cwd = process.cwd();
const reportDirPath = path.join(cwd, '.owl', 'report');
const screenshotsDirPath = path.join(cwd, '.owl', 'latest', platform);
const screenshots = await fs.readdir(screenshotsDirPath);

logger.info(`[OWL] Generating Report`);

const reportFilename = 'index.html';
const entryFile = path.join(__dirname, 'report', reportFilename);
const htmlTemplate = await fs.readFile(entryFile, 'utf-8');
const templateScript = handlebars.compile(htmlTemplate);
const htmlContent = templateScript({
currentYear: new Date().getFullYear(),
currentDateTime: new Date().toISOString(),
platform,
screenshots,
});

await fs.mkdir(reportDirPath, { recursive: true });
const reportFilePath = path.join(reportDirPath, 'index.html');
await fs.writeFile(reportFilePath, htmlContent);

logger.info(`[OWL] Report was built at ${reportDirPath}/${reportFilename}`);
};
Loading