Skip to content

Commit 27e8c46

Browse files
xcuitest improvements
1 parent def8afe commit 27e8c46

File tree

4 files changed

+1664
-40
lines changed

4 files changed

+1664
-40
lines changed

src/cli.ts

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import EspressoOptions, {
77
ReportFormat as EspressoReportFormat,
88
ThrottleNetwork as EspressoThrottleNetwork,
99
} from './models/espresso_options';
10-
import XCUITestOptions from './models/xcuitest_options';
10+
import XCUITestOptions, {
11+
Orientation as XCUITestOrientation,
12+
ThrottleNetwork as XCUITestThrottleNetwork,
13+
ReportFormat as XCUITestReportFormat,
14+
} from './models/xcuitest_options';
1115
import XCUITest from './providers/xcuitest';
1216
import packageJson from '../package.json';
1317
import MaestroOptions, {
@@ -360,17 +364,100 @@ const maestroCommand = program
360364
})
361365
.showHelpAfterError(true);
362366

363-
program
367+
const xcuitestCommand = program
364368
.command('xcuitest')
365-
.description('Bootstrap an XCUITest project.')
366-
.requiredOption('--app <string>', 'Path to application under test.')
367-
.requiredOption('--device <device>', 'Real device to use for testing.')
368-
.requiredOption('--test-app <string>', 'Path to test application.')
369+
.description('Run XCUITest tests on TestingBot.')
370+
.argument('[appFile]', 'Path to application IPA file')
371+
.argument('[testAppFile]', 'Path to test ZIP file containing XCUITests')
372+
// App and test options
373+
.option('--app <path>', 'Path to application IPA file.')
374+
.option('--test-app <path>', 'Path to test ZIP file containing XCUITests.')
375+
// Device configuration
376+
.option(
377+
'--device <device>',
378+
'Device name to use for testing (e.g., "iPhone 15", "iPad.*").',
379+
)
380+
.option('--platform-version <version>', 'iOS version (e.g., "17.0", "18.2").')
381+
.option('--real-device', 'Use a real device instead of a simulator.')
382+
.option('--tablet-only', 'Only allocate tablet devices.')
383+
.option('--phone-only', 'Only allocate phone devices.')
384+
.option(
385+
'--orientation <orientation>',
386+
'Screen orientation: PORTRAIT or LANDSCAPE.',
387+
(val) => val.toUpperCase() as XCUITestOrientation,
388+
)
389+
.option('--locale <locale>', 'Device locale (e.g., "DE", "US").')
390+
.option(
391+
'--timezone <timezone>',
392+
'Device timezone (e.g., "New_York", "Europe/London").',
393+
)
394+
// Test metadata
395+
.option('--name <name>', 'Test name for identification in dashboard.')
396+
.option('--build <build>', 'Build identifier for grouping test runs.')
397+
// Localization
398+
.option(
399+
'--language <lang>',
400+
'App language (ISO 639-1 code, e.g., "en", "fr", "de").',
401+
)
402+
// Geolocation
403+
.option(
404+
'--geo-location <code>',
405+
'Geographic IP location (ISO country code, e.g., "US", "DE").',
406+
)
407+
// Network throttling
408+
.option(
409+
'--throttle-network <speed>',
410+
'Network throttling: 4G, 3G, Edge, or airplane.',
411+
(val) => val as XCUITestThrottleNetwork,
412+
)
413+
// Execution mode
414+
.option('-q, --quiet', 'Quieter console output without progress updates.')
415+
.option(
416+
'--async',
417+
'Start tests and exit immediately without waiting for results.',
418+
)
419+
// Report options
420+
.option(
421+
'--report <format>',
422+
'Download test report after completion: html or junit.',
423+
(val) => val.toLowerCase() as XCUITestReportFormat,
424+
)
425+
.option(
426+
'--report-output-dir <path>',
427+
'Directory to save test reports (required when --report is used).',
428+
)
429+
// Authentication
369430
.option('--api-key <key>', 'TestingBot API key.')
370431
.option('--api-secret <secret>', 'TestingBot API secret.')
371-
.action(async (args) => {
432+
.action(async (appFileArg, testAppFileArg, args) => {
372433
try {
373-
const options = new XCUITestOptions(args.app, args.testApp, args.device);
434+
// Positional arguments take precedence, fall back to options
435+
const app = appFileArg || args.app;
436+
const testApp = testAppFileArg || args.testApp;
437+
438+
if (!app || !testApp) {
439+
xcuitestCommand.help();
440+
return;
441+
}
442+
443+
const options = new XCUITestOptions(app, testApp, args.device, {
444+
version: args.platformVersion,
445+
realDevice: args.realDevice,
446+
tabletOnly: args.tabletOnly,
447+
phoneOnly: args.phoneOnly,
448+
name: args.name,
449+
build: args.build,
450+
orientation: args.orientation,
451+
language: args.language,
452+
locale: args.locale,
453+
timeZone: args.timezone,
454+
geoLocation: args.geoLocation,
455+
throttleNetwork: args.throttleNetwork,
456+
quiet: args.quiet,
457+
async: args.async,
458+
report: args.report,
459+
reportOutputDir: args.reportOutputDir,
460+
});
374461
const credentials = await Auth.getCredentials({
375462
apiKey: args.apiKey,
376463
apiSecret: args.apiSecret,
@@ -381,13 +468,18 @@ program
381468
);
382469
}
383470
const xcuitest = new XCUITest(credentials, options);
384-
await xcuitest.run();
471+
const result = await xcuitest.run();
472+
if (!result.success) {
473+
process.exitCode = 1;
474+
}
385475
} catch (err) {
386476
logger.error(
387477
`XCUITest error: ${err instanceof Error ? err.message : err}`,
388478
);
479+
process.exitCode = 1;
389480
}
390-
});
481+
})
482+
.showHelpAfterError(true);
391483

392484
program
393485
.command('login')

src/models/xcuitest_options.ts

Lines changed: 197 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,106 @@
1+
export type Orientation = 'PORTRAIT' | 'LANDSCAPE';
2+
export type ReportFormat = 'html' | 'junit';
3+
export type ThrottleNetwork = '4G' | '3G' | 'Edge' | 'airplane';
4+
5+
export interface CustomNetworkProfile {
6+
uploadSpeed: number; // kbps
7+
downloadSpeed: number; // kbps
8+
latency: number; // ms
9+
loss: number; // percentage
10+
}
11+
12+
export interface XCUITestCapabilities {
13+
platformName: 'iOS';
14+
deviceName: string;
15+
version?: string;
16+
realDevice?: string;
17+
tabletOnly?: boolean;
18+
phoneOnly?: boolean;
19+
name?: string;
20+
build?: string;
21+
}
22+
23+
export interface XCUITestRunOptions {
24+
// Screen orientation
25+
orientation?: Orientation;
26+
// Localization
27+
language?: string;
28+
locale?: string;
29+
timeZone?: string;
30+
// Geolocation
31+
geoLocation?: string;
32+
// Network throttling
33+
throttle_network?: ThrottleNetwork | CustomNetworkProfile;
34+
}
35+
136
export default class XCUITestOptions {
237
private _app: string;
338
private _testApp: string;
4-
private _device: string;
39+
private _device?: string;
40+
private _version?: string;
41+
private _realDevice: boolean;
42+
private _tabletOnly: boolean;
43+
private _phoneOnly: boolean;
44+
private _name?: string;
45+
private _build?: string;
46+
// Screen orientation
47+
private _orientation?: Orientation;
48+
// Localization
49+
private _language?: string;
50+
private _locale?: string;
51+
private _timeZone?: string;
52+
// Geolocation
53+
private _geoLocation?: string;
54+
// Network throttling
55+
private _throttleNetwork?: ThrottleNetwork | CustomNetworkProfile;
56+
// Execution mode
57+
private _quiet: boolean;
58+
private _async: boolean;
59+
private _report?: ReportFormat;
60+
private _reportOutputDir?: string;
561

6-
public constructor(app: string, testApp: string, device: string) {
62+
public constructor(
63+
app: string,
64+
testApp: string,
65+
device?: string,
66+
options?: {
67+
version?: string;
68+
realDevice?: boolean;
69+
tabletOnly?: boolean;
70+
phoneOnly?: boolean;
71+
name?: string;
72+
build?: string;
73+
orientation?: Orientation;
74+
language?: string;
75+
locale?: string;
76+
timeZone?: string;
77+
geoLocation?: string;
78+
throttleNetwork?: ThrottleNetwork | CustomNetworkProfile;
79+
quiet?: boolean;
80+
async?: boolean;
81+
report?: ReportFormat;
82+
reportOutputDir?: string;
83+
},
84+
) {
785
this._app = app;
886
this._testApp = testApp;
987
this._device = device;
88+
this._version = options?.version;
89+
this._realDevice = options?.realDevice ?? false;
90+
this._tabletOnly = options?.tabletOnly ?? false;
91+
this._phoneOnly = options?.phoneOnly ?? false;
92+
this._name = options?.name;
93+
this._build = options?.build;
94+
this._orientation = options?.orientation;
95+
this._language = options?.language;
96+
this._locale = options?.locale;
97+
this._timeZone = options?.timeZone;
98+
this._geoLocation = options?.geoLocation;
99+
this._throttleNetwork = options?.throttleNetwork;
100+
this._quiet = options?.quiet ?? false;
101+
this._async = options?.async ?? false;
102+
this._report = options?.report;
103+
this._reportOutputDir = options?.reportOutputDir;
10104
}
11105

12106
public get app(): string {
@@ -17,7 +111,107 @@ export default class XCUITestOptions {
17111
return this._testApp;
18112
}
19113

20-
public get device(): string {
114+
public get device(): string | undefined {
21115
return this._device;
22116
}
117+
118+
public get version(): string | undefined {
119+
return this._version;
120+
}
121+
122+
public get realDevice(): boolean {
123+
return this._realDevice;
124+
}
125+
126+
public get tabletOnly(): boolean {
127+
return this._tabletOnly;
128+
}
129+
130+
public get phoneOnly(): boolean {
131+
return this._phoneOnly;
132+
}
133+
134+
public get name(): string | undefined {
135+
return this._name;
136+
}
137+
138+
public get build(): string | undefined {
139+
return this._build;
140+
}
141+
142+
public get orientation(): Orientation | undefined {
143+
return this._orientation;
144+
}
145+
146+
public get language(): string | undefined {
147+
return this._language;
148+
}
149+
150+
public get locale(): string | undefined {
151+
return this._locale;
152+
}
153+
154+
public get timeZone(): string | undefined {
155+
return this._timeZone;
156+
}
157+
158+
public get geoLocation(): string | undefined {
159+
return this._geoLocation;
160+
}
161+
162+
public get throttleNetwork():
163+
| ThrottleNetwork
164+
| CustomNetworkProfile
165+
| undefined {
166+
return this._throttleNetwork;
167+
}
168+
169+
public get quiet(): boolean {
170+
return this._quiet;
171+
}
172+
173+
public get async(): boolean {
174+
return this._async;
175+
}
176+
177+
public get report(): ReportFormat | undefined {
178+
return this._report;
179+
}
180+
181+
public get reportOutputDir(): string | undefined {
182+
return this._reportOutputDir;
183+
}
184+
185+
public getCapabilities(): XCUITestCapabilities {
186+
const caps: XCUITestCapabilities = {
187+
platformName: 'iOS',
188+
deviceName: this._device || '*',
189+
};
190+
191+
if (this._version) caps.version = this._version;
192+
if (this._realDevice) caps.realDevice = 'true';
193+
if (this._tabletOnly) caps.tabletOnly = true;
194+
if (this._phoneOnly) caps.phoneOnly = true;
195+
if (this._name) caps.name = this._name;
196+
if (this._build) caps.build = this._build;
197+
198+
return caps;
199+
}
200+
201+
public getXCUITestOptions(): XCUITestRunOptions | undefined {
202+
const opts: XCUITestRunOptions = {};
203+
204+
// Screen orientation
205+
if (this._orientation) opts.orientation = this._orientation;
206+
// Localization
207+
if (this._language) opts.language = this._language;
208+
if (this._locale) opts.locale = this._locale;
209+
if (this._timeZone) opts.timeZone = this._timeZone;
210+
// Geolocation
211+
if (this._geoLocation) opts.geoLocation = this._geoLocation;
212+
// Network throttling
213+
if (this._throttleNetwork) opts.throttle_network = this._throttleNetwork;
214+
215+
return Object.keys(opts).length > 0 ? opts : undefined;
216+
}
23217
}

0 commit comments

Comments
 (0)