Skip to content

Commit 605375c

Browse files
authored
feat: auto-detect bundle ID from iOS/Android projects (#3)
Add a third prompt question that asks for the app bundle ID. The default is auto-detected by scanning for Xcode projects (PRODUCT_BUNDLE_IDENTIFIER in .pbxproj) or Android projects (applicationId/namespace in build.gradle). iOS is preferred when both exist. If cwd is test/tests, the parent directory is also searched. The bundleId is written into mobilewright.config instead of test.use() in the example test, since the fixture now reads it from config.
1 parent 11fd4d2 commit 605375c

1 file changed

Lines changed: 68 additions & 6 deletions

File tree

src/index.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,59 @@ import { execSync } from "child_process";
77

88
type Language = "ts" | "js";
99

10+
function getSearchDirs(cwd: string): string[] {
11+
const dirs = [cwd];
12+
const basename = path.basename(cwd).toLowerCase();
13+
if (basename === "test" || basename === "tests") {
14+
dirs.push(path.dirname(cwd));
15+
}
16+
return dirs;
17+
}
18+
19+
function detectIosBundleId(dirs: string[]): string | undefined {
20+
for (const dir of dirs) {
21+
const entries = fs.readdirSync(dir);
22+
for (const entry of entries) {
23+
if (entry.endsWith(".xcodeproj")) {
24+
const pbxprojPath = path.join(dir, entry, "project.pbxproj");
25+
if (fs.existsSync(pbxprojPath)) {
26+
const content = fs.readFileSync(pbxprojPath, "utf-8");
27+
const match = content.match(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*"?([^";]+)"?\s*;/);
28+
if (match) {
29+
return match[1].trim();
30+
}
31+
}
32+
}
33+
}
34+
}
35+
return undefined;
36+
}
37+
38+
function detectAndroidPackageName(dirs: string[]): string | undefined {
39+
for (const dir of dirs) {
40+
const appDir = path.join(dir, "app");
41+
if (!fs.existsSync(appDir)) continue;
42+
43+
for (const filename of ["build.gradle.kts", "build.gradle"]) {
44+
const gradlePath = path.join(appDir, filename);
45+
if (!fs.existsSync(gradlePath)) continue;
46+
47+
const content = fs.readFileSync(gradlePath, "utf-8");
48+
const appIdMatch = content.match(/applicationId\s*=?\s*"([^"]+)"/);
49+
if (appIdMatch) return appIdMatch[1];
50+
51+
const namespaceMatch = content.match(/namespace\s*=?\s*"([^"]+)"/);
52+
if (namespaceMatch) return namespaceMatch[1];
53+
}
54+
}
55+
return undefined;
56+
}
57+
58+
function detectBundleId(): string {
59+
const dirs = getSearchDirs(process.cwd());
60+
return detectIosBundleId(dirs) ?? detectAndroidPackageName(dirs) ?? "";
61+
}
62+
1063
function createPackageJson(targetDir: string, language: Language): void {
1164
const pkgPath = path.join(targetDir, "package.json");
1265

@@ -32,7 +85,7 @@ function createPackageJson(targetDir: string, language: Language): void {
3285
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
3386
}
3487

35-
function createConfigFile(targetDir: string, testDir: string, language: Language): void {
88+
function createConfigFile(targetDir: string, testDir: string, language: Language, bundleId: string): void {
3689
const ext = language === "ts" ? "ts" : "js";
3790
const configPath = path.join(targetDir, `mobilewright.config.${ext}`);
3891

@@ -44,9 +97,11 @@ function createConfigFile(targetDir: string, testDir: string, language: Language
4497
? "export default defineConfig"
4598
: "module.exports = defineConfig";
4699

100+
const bundleIdLine = bundleId ? `\n bundleId: '${bundleId}',` : "";
101+
47102
const content = `${importLine}
48103
${exportLine}({
49-
testDir: './${testDir}',
104+
testDir: './${testDir}',${bundleIdLine}
50105
reporter: 'html',
51106
});
52107
`;
@@ -64,8 +119,6 @@ function createTestFile(targetDir: string, testDir: string, language: Language):
64119

65120
const content = `${importLine}
66121
67-
test.use({ bundleId: "com.example.app" });
68-
69122
test('app launches and shows home screen', async ({ screen }) => {
70123
await expect(screen.getByText('Welcome')).toBeVisible();
71124
});
@@ -92,6 +145,8 @@ async function main() {
92145
"Getting started with writing mobile automation and end-to-end tests"
93146
);
94147

148+
const detectedBundleId = detectBundleId();
149+
95150
const response = await prompts(
96151
[
97152
{
@@ -110,6 +165,12 @@ async function main() {
110165
message: "Directory name for test files?",
111166
initial: "tests",
112167
},
168+
{
169+
type: "text",
170+
name: "bundleId",
171+
message: "What is the app bundle ID to test? (Leave empty to skip)",
172+
initial: detectedBundleId,
173+
},
113174
],
114175
{
115176
onCancel: () => {
@@ -118,9 +179,10 @@ async function main() {
118179
}
119180
);
120181

121-
const { language, testDir } = response as {
182+
const { language, testDir, bundleId } = response as {
122183
language: Language;
123184
testDir: string;
185+
bundleId: string;
124186
};
125187

126188
if (!language || !testDir) {
@@ -130,7 +192,7 @@ async function main() {
130192
const targetDir = process.cwd();
131193

132194
createPackageJson(targetDir, language);
133-
createConfigFile(targetDir, testDir, language);
195+
createConfigFile(targetDir, testDir, language, bundleId);
134196
createTestFile(targetDir, testDir, language);
135197

136198
runNpmInstall(targetDir);

0 commit comments

Comments
 (0)