|
1 | 1 | import childProcess from "child_process";
|
2 | 2 | import logger from "../../logger";
|
| 3 | +import { getAppLiveData } from "./device-cache"; |
| 4 | +import { fuzzySearchDevices } from "./fuzzy-search"; |
3 | 5 | import { sanitizeUrlParam } from "../../lib/utils";
|
| 6 | +import { uploadApp } from "./upload-app"; |
| 7 | + |
| 8 | +export interface DeviceEntry { |
| 9 | + device: string; |
| 10 | + display_name: string; |
| 11 | + os: string; |
| 12 | + os_version: string; |
| 13 | + real_mobile: boolean; |
| 14 | +} |
4 | 15 |
|
5 | 16 | interface StartSessionArgs {
|
6 |
| - appUrl: string; |
| 17 | + appPath: string; |
7 | 18 | desiredPlatform: "android" | "ios";
|
8 | 19 | desiredPhone: string;
|
9 | 20 | desiredPlatformVersion: string;
|
10 | 21 | }
|
11 | 22 |
|
| 23 | +/** |
| 24 | + * Starts an App Live session after filtering, fuzzy matching, and launching. |
| 25 | + */ |
12 | 26 | export async function startSession(args: StartSessionArgs): Promise<string> {
|
13 |
| - // Sanitize all input parameters |
14 |
| - const sanitizedArgs = { |
15 |
| - appUrl: sanitizeUrlParam(args.appUrl), |
16 |
| - desiredPlatform: sanitizeUrlParam(args.desiredPlatform), |
17 |
| - desiredPhone: sanitizeUrlParam(args.desiredPhone), |
18 |
| - desiredPlatformVersion: sanitizeUrlParam(args.desiredPlatformVersion), |
19 |
| - }; |
20 |
| - |
21 |
| - // Get app hash ID and format phone name |
22 |
| - const appHashedId = sanitizedArgs.appUrl.split("bs://").pop(); |
23 |
| - const desiredPhoneWithSpaces = sanitizedArgs.desiredPhone.replace( |
24 |
| - /\s+/g, |
25 |
| - "+", |
| 27 | + const { appPath, desiredPlatform, desiredPhone } = args; |
| 28 | + let { desiredPlatformVersion } = args; |
| 29 | + |
| 30 | + const data = await getAppLiveData(); |
| 31 | + const allDevices: DeviceEntry[] = data.mobile.flatMap((group: any) => |
| 32 | + group.devices.map((dev: any) => ({ ...dev, os: group.os })), |
| 33 | + ); |
| 34 | + |
| 35 | + // Exact filter by platform and version |
| 36 | + if ( |
| 37 | + desiredPlatformVersion == "latest" || |
| 38 | + desiredPlatformVersion == "oldest" |
| 39 | + ) { |
| 40 | + const filtered = allDevices.filter((d) => d.os === desiredPlatform); |
| 41 | + filtered.sort((a, b) => { |
| 42 | + const versionA = parseFloat(a.os_version); |
| 43 | + const versionB = parseFloat(b.os_version); |
| 44 | + return desiredPlatformVersion === "latest" |
| 45 | + ? versionB - versionA // descending for "latest" |
| 46 | + : versionA - versionB; // ascending for specific version |
| 47 | + }); |
| 48 | + |
| 49 | + const requiredVersion = filtered[0].os_version; |
| 50 | + |
| 51 | + desiredPlatformVersion = requiredVersion; |
| 52 | + } |
| 53 | + const filtered = allDevices.filter( |
| 54 | + (d) => d.os === desiredPlatform && d.os_version === desiredPlatformVersion, |
| 55 | + ); |
| 56 | + |
| 57 | + // Fuzzy match |
| 58 | + const matches = await fuzzySearchDevices(filtered, desiredPhone); |
| 59 | + |
| 60 | + if (matches.length === 0) { |
| 61 | + throw new Error( |
| 62 | + `No devices found matching "${desiredPhone}" for ${desiredPlatform} ${desiredPlatformVersion} ${JSON.stringify(matches, null, 2)}`, |
| 63 | + ); |
| 64 | + } |
| 65 | + const exactMatch = matches.find( |
| 66 | + (d) => d.display_name.toLowerCase() === desiredPhone.toLowerCase(), |
| 67 | + ); |
| 68 | + |
| 69 | + if (exactMatch) { |
| 70 | + matches.splice(0, matches.length, exactMatch); // Replace matches with the exact match |
| 71 | + } else if (matches.length > 1) { |
| 72 | + const names = matches.map((d) => d.display_name).join(", "); |
| 73 | + throw new Error( |
| 74 | + `Multiple devices found: [${names}]. Select one out of them.`, |
| 75 | + ); |
| 76 | + } |
| 77 | + |
| 78 | + const { app_url } = await uploadApp(appPath); |
| 79 | + |
| 80 | + if (!app_url.match("bs://")) { |
| 81 | + throw new Error("The app path is not a valid BrowserStack app URL."); |
| 82 | + } |
| 83 | + |
| 84 | + const device = matches[0]; |
| 85 | + const deviceParam = sanitizeUrlParam( |
| 86 | + device.display_name.replace(/\s+/g, "+"), |
26 | 87 | );
|
27 | 88 |
|
28 |
| - // Construct URL with encoded parameters |
29 | 89 | const params = new URLSearchParams({
|
30 |
| - os: sanitizedArgs.desiredPlatform, |
31 |
| - os_version: sanitizedArgs.desiredPlatformVersion, |
32 |
| - app_hashed_id: appHashedId || "", |
| 90 | + os: desiredPlatform, |
| 91 | + os_version: desiredPlatformVersion, |
| 92 | + app_hashed_id: app_url.split("bs://").pop() || "", |
33 | 93 | scale_to_fit: "true",
|
34 | 94 | speed: "1",
|
35 | 95 | start: "true",
|
36 | 96 | });
|
37 |
| - |
38 |
| - const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${desiredPhoneWithSpaces}`; |
| 97 | + const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`; |
39 | 98 |
|
40 | 99 | try {
|
41 | 100 | // Use platform-specific commands with proper escaping
|
|
0 commit comments