Skip to content

Commit e865c90

Browse files
update file type detector
1 parent 38f3ede commit e865c90

3 files changed

Lines changed: 153 additions & 59 deletions

File tree

src/providers/maestro.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,28 @@ export default class Maestro extends BaseProvider<MaestroOptions> {
9191
super(credentials, options);
9292
}
9393

94+
private static readonly SUPPORTED_APP_EXTENSIONS = [
95+
'.apk',
96+
'.apks',
97+
'.ipa',
98+
'.app',
99+
'.zip',
100+
];
101+
94102
private async validate(): Promise<boolean> {
95103
if (this.options.app === undefined) {
96104
throw new TestingBotError(`app option is required`);
97105
}
98106

107+
// Validate app file extension
108+
const appExt = path.extname(this.options.app).toLowerCase();
109+
if (!Maestro.SUPPORTED_APP_EXTENSIONS.includes(appExt)) {
110+
throw new TestingBotError(
111+
`Unsupported app file format: ${appExt || '(no extension)'}. ` +
112+
`Supported formats: ${Maestro.SUPPORTED_APP_EXTENSIONS.join(', ')}`,
113+
);
114+
}
115+
99116
if (this.options.flows === undefined || this.options.flows.length === 0) {
100117
throw new TestingBotError(`flows option is required`);
101118
}

src/utils/file-type-detector.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import path from 'node:path';
2+
13
export interface FileTypeResult {
24
ext: string;
35
mime: string;
@@ -20,28 +22,51 @@ export async function detectFileType(
2022
}
2123
}
2224

25+
/**
26+
* Get file extension in lowercase without the dot
27+
*/
28+
function getExtension(filePath: string): string {
29+
return path.extname(filePath).toLowerCase().slice(1);
30+
}
31+
2332
/**
2433
* Detect platform (Android or iOS) from app file.
25-
* Uses magic bytes for content detection, with extension fallback for zip-based formats.
34+
* Uses a combination of magic bytes and file extension for reliable detection.
35+
*
36+
* Android: .apk, .apks files
37+
* iOS: .ipa, .app, .zip files (when not APK)
2638
*/
2739
export async function detectPlatformFromFile(
2840
filePath: string,
2941
): Promise<'Android' | 'iOS' | undefined> {
42+
const ext = getExtension(filePath);
3043
const fileType = await detectFileType(filePath);
3144

45+
// Check for Android APK files
3246
if (fileType) {
33-
// APK files are detected as 'application/zip' with ext 'apk'
34-
// or as 'application/vnd.android.package-archive'
3547
if (
3648
fileType.ext === 'apk' ||
3749
fileType.mime === 'application/vnd.android.package-archive'
3850
) {
3951
return 'Android';
4052
}
53+
}
4154

42-
if (fileType.ext === 'zip' || fileType.mime === 'application/zip') {
43-
return 'iOS';
44-
}
55+
// Extension-based detection (more reliable for mobile apps)
56+
// APK and APKS are Android
57+
if (ext === 'apk' || ext === 'apks') {
58+
return 'Android';
59+
}
60+
61+
// IPA, APP are iOS
62+
if (ext === 'ipa' || ext === 'app') {
63+
return 'iOS';
64+
}
65+
66+
// ZIP files could be either, but commonly used for iOS simulator builds
67+
// If magic bytes detected it as zip and extension is .zip, assume iOS
68+
if (ext === 'zip' && fileType?.mime === 'application/zip') {
69+
return 'iOS';
4570
}
4671

4772
return undefined;

tests/utils/file-type-detector.test.ts

Lines changed: 105 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,95 +9,147 @@
99
import type { FileTypeResult } from '../../src/utils/file-type-detector';
1010

1111
// Helper to create a testable version of detectPlatformFromFile logic
12+
// This mirrors the logic in the actual implementation
1213
function determinePlatform(
14+
filePath: string,
1315
fileType: FileTypeResult | undefined,
1416
): 'Android' | 'iOS' | undefined {
17+
const ext = filePath.split('.').pop()?.toLowerCase() || '';
18+
19+
// Check for Android APK files via magic bytes
1520
if (fileType) {
1621
if (
1722
fileType.ext === 'apk' ||
1823
fileType.mime === 'application/vnd.android.package-archive'
1924
) {
2025
return 'Android';
2126
}
27+
}
2228

23-
if (fileType.ext === 'zip' || fileType.mime === 'application/zip') {
24-
return 'iOS';
25-
}
29+
// Extension-based detection
30+
if (ext === 'apk' || ext === 'apks') {
31+
return 'Android';
32+
}
33+
34+
if (ext === 'ipa' || ext === 'app') {
35+
return 'iOS';
36+
}
37+
38+
// ZIP files with zip mime type are assumed iOS
39+
if (ext === 'zip' && fileType?.mime === 'application/zip') {
40+
return 'iOS';
2641
}
2742

2843
return undefined;
2944
}
3045

3146
describe('file-type-detector', () => {
3247
describe('platform detection logic', () => {
33-
it('should detect Android for APK files (by ext)', () => {
34-
const result = determinePlatform({
35-
ext: 'apk',
36-
mime: 'application/vnd.android.package-archive',
48+
describe('Android detection', () => {
49+
it('should detect Android for APK files (by magic bytes)', () => {
50+
const result = determinePlatform('app.apk', {
51+
ext: 'apk',
52+
mime: 'application/vnd.android.package-archive',
53+
});
54+
expect(result).toBe('Android');
3755
});
38-
expect(result).toBe('Android');
39-
});
4056

41-
it('should detect Android for files with android package mime type', () => {
42-
const result = determinePlatform({
43-
ext: 'unknown',
44-
mime: 'application/vnd.android.package-archive',
57+
it('should detect Android for files with android package mime type', () => {
58+
const result = determinePlatform('app.unknown', {
59+
ext: 'unknown',
60+
mime: 'application/vnd.android.package-archive',
61+
});
62+
expect(result).toBe('Android');
4563
});
46-
expect(result).toBe('Android');
47-
});
4864

49-
it('should detect iOS for zip files (by ext)', () => {
50-
const result = determinePlatform({
51-
ext: 'zip',
52-
mime: 'application/zip',
65+
it('should detect Android for .apk extension (without magic bytes)', () => {
66+
const result = determinePlatform('app.apk', undefined);
67+
expect(result).toBe('Android');
5368
});
54-
expect(result).toBe('iOS');
55-
});
5669

57-
it('should detect iOS for files with zip mime type', () => {
58-
const result = determinePlatform({
59-
ext: 'unknown',
60-
mime: 'application/zip',
70+
it('should detect Android for .apks extension', () => {
71+
const result = determinePlatform('app.apks', undefined);
72+
expect(result).toBe('Android');
6173
});
62-
expect(result).toBe('iOS');
63-
});
6474

65-
it('should return undefined when file type is undefined', () => {
66-
const result = determinePlatform(undefined);
67-
expect(result).toBeUndefined();
75+
it('should prioritize apk ext over mime type', () => {
76+
// Even with a wrong mime type, apk ext should return Android
77+
const result = determinePlatform('app.apk', {
78+
ext: 'apk',
79+
mime: 'application/zip',
80+
});
81+
expect(result).toBe('Android');
82+
});
6883
});
6984

70-
it('should return undefined for non-mobile file types', () => {
71-
const result = determinePlatform({
72-
ext: 'pdf',
73-
mime: 'application/pdf',
85+
describe('iOS detection', () => {
86+
it('should detect iOS for .ipa extension', () => {
87+
const result = determinePlatform('app.ipa', {
88+
ext: 'zip',
89+
mime: 'application/zip',
90+
});
91+
expect(result).toBe('iOS');
7492
});
75-
expect(result).toBeUndefined();
76-
});
7793

78-
it('should return undefined for image files', () => {
79-
const result = determinePlatform({
80-
ext: 'png',
81-
mime: 'image/png',
94+
it('should detect iOS for .ipa extension (without magic bytes)', () => {
95+
const result = determinePlatform('app.ipa', undefined);
96+
expect(result).toBe('iOS');
97+
});
98+
99+
it('should detect iOS for .app extension', () => {
100+
const result = determinePlatform('MyApp.app', undefined);
101+
expect(result).toBe('iOS');
82102
});
83-
expect(result).toBeUndefined();
84-
});
85103

86-
it('should return undefined for text files', () => {
87-
const result = determinePlatform({
88-
ext: 'txt',
89-
mime: 'text/plain',
104+
it('should detect iOS for .zip files with zip mime type', () => {
105+
const result = determinePlatform('app.zip', {
106+
ext: 'zip',
107+
mime: 'application/zip',
108+
});
109+
expect(result).toBe('iOS');
110+
});
111+
112+
it('should detect iOS for uppercase IPA extension', () => {
113+
const result = determinePlatform('app.IPA', undefined);
114+
expect(result).toBe('iOS');
90115
});
91-
expect(result).toBeUndefined();
92116
});
93117

94-
it('should prioritize apk ext over mime type', () => {
95-
// Even with a wrong mime type, apk ext should return Android
96-
const result = determinePlatform({
97-
ext: 'apk',
98-
mime: 'application/zip',
118+
describe('undefined cases', () => {
119+
it('should return undefined when file type is undefined and no matching extension', () => {
120+
const result = determinePlatform('file.unknown', undefined);
121+
expect(result).toBeUndefined();
122+
});
123+
124+
it('should return undefined for non-mobile file types', () => {
125+
const result = determinePlatform('document.pdf', {
126+
ext: 'pdf',
127+
mime: 'application/pdf',
128+
});
129+
expect(result).toBeUndefined();
130+
});
131+
132+
it('should return undefined for image files', () => {
133+
const result = determinePlatform('image.png', {
134+
ext: 'png',
135+
mime: 'image/png',
136+
});
137+
expect(result).toBeUndefined();
138+
});
139+
140+
it('should return undefined for text files', () => {
141+
const result = determinePlatform('readme.txt', {
142+
ext: 'txt',
143+
mime: 'text/plain',
144+
});
145+
expect(result).toBeUndefined();
146+
});
147+
148+
it('should return undefined for .zip without mime type detection', () => {
149+
// If magic bytes couldn't detect the file type, don't assume iOS
150+
const result = determinePlatform('flows.zip', undefined);
151+
expect(result).toBeUndefined();
99152
});
100-
expect(result).toBe('Android');
101153
});
102154
});
103155
});

0 commit comments

Comments
 (0)