Skip to content

Commit

Permalink
maestro provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jochen-testingbot committed Dec 22, 2024
1 parent 0dcf2cd commit e5ed3b0
Show file tree
Hide file tree
Showing 7 changed files with 613 additions and 74 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"clean": "rm -rf dist",
"start": "node dist/index.js",
"format": "prettier --write '**/*.{js,ts}'",
"test": "jest --detectOpenHandles",
"test": "jest",
"release": "release-it --github.release",
"release:ci": "npm run release -- --ci --npm.skipChecks --no-git.requireCleanWorkingDir",
"release:patch": "npm run release -- patch",
Expand Down
99 changes: 99 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Command } from 'commander';
import logger from './logger';
import auth from './auth';
import Espresso from './providers/espresso';
import EspressoOptions from './models/espresso_options';
import XCUITestOptions from './models/xcuitest_options';
import XCUITest from './providers/xcuitest';
import packageJson from '../package.json';
import MaestroOptions from './models/maestro_options';
import Maestro from './providers/maestro';

const program = new Command();

program
.version(packageJson.version)
.description(
'TestingBotCTL is a CLI-tool to run Espresso, XCUITest and Maestro tests in the TestingBot cloud',
);

program
.command('espresso')
.description('Bootstrap an Espresso project.')
.requiredOption('--app <string>', 'Path to application under test.')
.requiredOption('--device <device>', 'Real device to use for testing.')
.requiredOption(
'--emulator <emulator>',
'Android emulator to use for testing.',
)
.requiredOption('--test-app <string>', 'Path to test application.')
.action(async (args) => {
try {
const options = new EspressoOptions(
args.app,
args.testApp,
args.device,
args.emulator,
);
const credentials = await auth.getCredentials();
if (credentials === null) {
throw new Error('Please specify credentials');
}
const espresso = new Espresso(credentials, options);
await espresso.run();
} catch (err: any) {
logger.error(`Espresso error: ${err.message}`);
}
})
.showHelpAfterError(true);

program
.command('maestro')
.description('Bootstrap a Maestro project.')
.requiredOption('--app <string>', 'Path to application under test.')
.requiredOption(
'--device <device>',
'Android emulator or iOS Simulator to use for testing.',
)
.requiredOption('--test-app <string>', 'Path to test application.')
.action(async (args) => {
try {
const options = new MaestroOptions(
args.app,
args.testApp,
args.device,
args.emulator,
);
const credentials = await auth.getCredentials();
if (credentials === null) {
throw new Error('Please specify credentials');
}
const maestto = new Maestro(credentials, options);
await maestto.run();
} catch (err: any) {
logger.error(`Maestro error: ${err.message}`);
}
})
.showHelpAfterError(true);

program
.command('xcuitest')
.description('Bootstrap an XCUITest project.')
.requiredOption('--app <string>', 'Path to application under test.')
.requiredOption('--device <device>', 'Real device to use for testing.')
.requiredOption('--test-app <string>', 'Path to test application.')
.action(async (args) => {
try {
const options = new XCUITestOptions(args.app, args.testApp, args.device);
const credentials = await auth.getCredentials();
if (credentials === null) {
throw new Error('Please specify credentials');
}
const xcuitest = new XCUITest(credentials, options);
await xcuitest.run();
} catch (err: any) {
logger.error(`XCUITest error: ${err.message}`);
}
});

export default program;
74 changes: 1 addition & 73 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,2 @@
import { Command } from 'commander';
import logger from './logger';
import auth from './auth';
import Espresso from './providers/espresso';
import EspressoOptions from './models/espresso_options';
import XCUITestOptions from './models/xcuitest_options';
import XCUITest from './providers/xcuitest';
import packageJson from '../package.json';

const program = new Command();

program
.version(packageJson.version)
.description(
'TestingBotCTL is a CLI-tool to run Espresso, XCUITest and Maestro tests in the TestingBot cloud',
);

program
.command('espresso')
.description('Bootstrap an Espresso project.')
.requiredOption('--app <string>', 'Path to application under test.')
.requiredOption('--device <device>', 'Real device to use for testing.')
.requiredOption(
'--emulator <emulator>',
'Android emulator to use for testing.',
)
.requiredOption('--test-app <string>', 'Path to test application.')
.action(async (args) => {
try {
const options = new EspressoOptions(
args.app,
args.testApp,
args.device,
args.emulator,
);
const credentials = await auth.getCredentials();
if (credentials === null) {
throw new Error('Please specify credentials');
}
const espresso = new Espresso(credentials, options);
await espresso.run();
} catch (err: any) {
logger.error(`Espresso error: ${err.message}`);
process.exit(1);
}
})
.showHelpAfterError(true);

program
.command('xcuitest')
.description('Bootstrap an XCUITest project.')
.requiredOption('--app <string>', 'Path to application under test.')
.requiredOption('--device <device>', 'Real device to use for testing.')
.requiredOption('--test-app <string>', 'Path to test application.')
.action(async (args) => {
try {
const options = new XCUITestOptions(args.app, args.testApp, args.device);
const credentials = await auth.getCredentials();
if (credentials === null) {
throw new Error('Please specify credentials');
}
const xcuitest = new XCUITest(credentials, options);
await xcuitest.run();
} catch (err: any) {
logger.error(`XCUITest error: ${err.message}`);
process.exit(1);
}
});

import program from './cli';
program.parse(process.argv);

auth.getCredentials().then((credentials: any) => {
logger.info(credentials.toString());
});
34 changes: 34 additions & 0 deletions src/models/maestro_options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default class MaestroOptions {
private _app: string;
private _testApp: string;
private _device: string;
private _emulator: string;

public constructor(
app: string,
testApp: string,
device: string,
emulator: string,
) {
this._app = app;
this._testApp = testApp;
this._device = device;
this._emulator = emulator;
}

public get app(): string {
return this._app;
}

public get testApp(): string {
return this._testApp;
}

public get device(): string {
return this._device;
}

public get emulator(): string {
return this._emulator;
}
}
171 changes: 171 additions & 0 deletions src/providers/maestro.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import MaestroOptions from '../models/maestro_options';
import logger from '../logger';
import Credentials from '../models/credentials';
import axios from 'axios';
import fs from 'node:fs';
import path from 'node:path';
import FormData from 'form-data';
import TestingBotError from '../models/testingbot_error';
import utils from '../utils';

export default class Maestro {
private readonly URL = 'https://api.testingbot.com/v1/app-automate/maestro';
private credentials: Credentials;
private options: MaestroOptions;

private appId: number | undefined = undefined;

public constructor(credentials: Credentials, options: MaestroOptions) {
this.credentials = credentials;
this.options = options;
}

private async validate(): Promise<boolean> {
if (this.options.app === undefined) {
throw new TestingBotError(`app option is required`);
}

try {
await fs.promises.access(this.options.app, fs.constants.R_OK);
} catch (err) {
throw new TestingBotError(
`Provided app path does not exist ${this.options.app}`,
);
}

if (this.options.testApp === undefined) {
throw new TestingBotError(`testApp option is required`);
}

try {
await fs.promises.access(this.options.testApp, fs.constants.R_OK);
} catch (err) {
throw new TestingBotError(
`testApp path does not exist ${this.options.testApp}`,
);
}

if (
this.options.device === undefined &&
this.options.emulator === undefined
) {
throw new TestingBotError(`Please specify either a device or emulator`);
}

return true;
}

public async run() {
if (!(await this.validate())) {
return;
}
try {
logger.info('Uploading Maestro App');
await this.uploadApp();

logger.info('Uploading Maestro Test App');
await this.uploadTestApp();

logger.info('Running Maestro Tests');
await this.runTests();
} catch (error: any) {
logger.error(error.message);
}
}

private async uploadApp() {
const fileName = path.basename(this.options.app);
const fileStream = fs.createReadStream(this.options.app);

const formData = new FormData();
formData.append('file', fileStream);
const response = await axios.post(`${this.URL}/app`, formData, {
headers: {
'Content-Type': 'application/vnd.android.package-archive',
'Content-Disposition': `attachment; filename=${fileName}`,
'User-Agent': utils.getUserAgent(),
},
auth: {
username: this.credentials.userName,
password: this.credentials.accessKey,
},
});

const result = response.data;
if (result.id) {
this.appId = result.id;
} else {
throw new TestingBotError(`Uploading app failed: ${result.error}`);
}

return true;
}

private async uploadTestApp() {
const fileName = path.basename(this.options.testApp);
const fileStream = fs.createReadStream(this.options.testApp);

const formData = new FormData();
formData.append('file', fileStream);
const response = await axios.post(
`${this.URL}/${this.appId}/tests`,
formData,
{
headers: {
'Content-Type': 'application/zip,',
'Content-Disposition': `attachment; filename=${fileName}`,
'User-Agent': utils.getUserAgent(),
},
auth: {
username: this.credentials.userName,
password: this.credentials.accessKey,
},
},
);

const result = response.data;
if (!result.id) {
throw new TestingBotError(`Uploading test app failed: ${result.error}`);
}

return true;
}

private async runTests() {
try {
const response = await axios.post(
`${this.URL}/${this.appId}/run`,
{
capabilities: [
{
deviceName: this.options.emulator,
},
],
},
{
headers: {
'Content-Type': 'application/json',
'User-Agent': utils.getUserAgent(),
},
auth: {
username: this.credentials.userName,
password: this.credentials.accessKey,
},
},
);

const result = response.data;
if (result.success === false) {
throw new TestingBotError(`Running Maestro test failed`, {
cause: result.error,
});
}

return true;
} catch (error) {
throw new TestingBotError(`Running Maestro test failed`, {
cause: error,
});
}
}
}
Loading

0 comments on commit e5ed3b0

Please sign in to comment.