Skip to content

Fix : Version Resolution and Device Search in App Live #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
32 changes: 32 additions & 0 deletions src/tools/applive-utils/device-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { customFuzzySearch } from "../../lib/fuzzy.js";
import { DeviceEntry } from "./types.js";

/**
* Find matching devices by name with exact match preference.
* Throws if none or multiple exact matches.
*/
export function findDeviceByName(
devices: DeviceEntry[],
desiredPhone: string,
): DeviceEntry[] {
const matches = customFuzzySearch(devices, ["display_name"], desiredPhone, 5);
if (matches.length === 0) {
const options = [...new Set(devices.map((d) => d.display_name))].join(", ");
throw new Error(
`No devices matching "${desiredPhone}". Available devices: ${options}`,
);
}
// Exact-case-insensitive filter
const exact = matches.filter(
(m) => m.display_name.toLowerCase() === desiredPhone.toLowerCase(),
);
if (exact) return exact;
// If no exact but multiple fuzzy, ask user
if (matches.length > 1) {
const names = matches.map((d) => d.display_name).join(", ");
throw new Error(
`Alternative Device/Device's found : ${names}. Please Select one.`,
);
}
return matches;
}
19 changes: 0 additions & 19 deletions src/tools/applive-utils/fuzzy-search.ts

This file was deleted.

214 changes: 42 additions & 172 deletions src/tools/applive-utils/start-session.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import childProcess from "child_process";
import logger from "../../logger.js";
import childProcess from "child_process";
import {
BrowserStackProducts,
getDevicesAndBrowsers,
BrowserStackProducts,
} from "../../lib/device-cache.js";
import { fuzzySearchDevices } from "./fuzzy-search.js";
import { sanitizeUrlParam } from "../../lib/utils.js";
import { uploadApp } from "./upload-app.js";

export interface DeviceEntry {
device: string;
display_name: string;
os: string;
os_version: string;
real_mobile: boolean;
}
import { findDeviceByName } from "./device-search.js";
import { pickVersion } from "./version-utils.js";
import { DeviceEntry } from "./types.js";

interface StartSessionArgs {
appPath: string;
Expand All @@ -24,192 +18,68 @@ interface StartSessionArgs {
}

/**
* Starts an App Live session after filtering, fuzzy matching, and launching.
* @param args - The arguments for starting the session.
* @returns The launch URL for the session.
* @throws Will throw an error if no devices are found or if the app URL is invalid.
* Start an App Live session: filter, select, upload, and open.
*/
export async function startSession(args: StartSessionArgs): Promise<string> {
const { appPath, desiredPlatform, desiredPhone } = args;
let { desiredPlatformVersion } = args;
const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } =
args;

// 1) Fetch devices for APP_LIVE
const data = await getDevicesAndBrowsers(BrowserStackProducts.APP_LIVE);

const allDevices: DeviceEntry[] = data.mobile.flatMap((group: any) =>
group.devices.map((dev: any) => ({ ...dev, os: group.os })),
);

desiredPlatformVersion = resolvePlatformVersion(
allDevices,
desiredPlatform,
desiredPlatformVersion,
);

const filteredDevices = filterDevicesByPlatformAndVersion(
allDevices,
desiredPlatform,
desiredPlatformVersion,
);

const matches = await fuzzySearchDevices(filteredDevices, desiredPhone);

const selectedDevice = validateAndSelectDevice(
matches,
desiredPhone,
desiredPlatform,
desiredPlatformVersion,
const all: DeviceEntry[] = data.mobile.flatMap((grp: any) =>
grp.devices.map((dev: any) => ({ ...dev, os: grp.os })),
);

const { app_url } = await uploadApp(appPath);

validateAppUrl(app_url);

const launchUrl = constructLaunchUrl(
app_url,
selectedDevice,
desiredPlatform,
desiredPlatformVersion,
);

openBrowser(launchUrl);

return launchUrl;
}

/**
* Resolves the platform version based on the desired platform and version.
* @param allDevices - The list of all devices.
* @param desiredPlatform - The desired platform (android or ios).
* @param desiredPlatformVersion - The desired platform version.
* @returns The resolved platform version.
* @throws Will throw an error if the platform version is not valid.
*/
function resolvePlatformVersion(
allDevices: DeviceEntry[],
desiredPlatform: string,
desiredPlatformVersion: string,
): string {
if (
desiredPlatformVersion === "latest" ||
desiredPlatformVersion === "oldest"
) {
const filtered = allDevices.filter((d) => d.os === desiredPlatform);
filtered.sort((a, b) => {
const versionA = parseFloat(a.os_version);
const versionB = parseFloat(b.os_version);
return desiredPlatformVersion === "latest"
? versionB - versionA
: versionA - versionB;
});

return filtered[0].os_version;
// 2) Filter by OS
const osMatches = all.filter((d) => d.os === desiredPlatform);
if (!osMatches.length) {
throw new Error(`No devices for OS "${desiredPlatform}"`);
}
return desiredPlatformVersion;
}

/**
* Filters devices based on the desired platform and version.
* @param allDevices - The list of all devices.
* @param desiredPlatform - The desired platform (android or ios).
* @param desiredPlatformVersion - The desired platform version.
* @returns The filtered list of devices.
* @throws Will throw an error if the platform version is not valid.
*/
function filterDevicesByPlatformAndVersion(
allDevices: DeviceEntry[],
desiredPlatform: string,
desiredPlatformVersion: string,
): DeviceEntry[] {
return allDevices.filter((d) => {
if (d.os !== desiredPlatform) return false;
// 3) Select by name
const nameMatches = findDeviceByName(osMatches, desiredPhone);

try {
const versionA = parseFloat(d.os_version);
const versionB = parseFloat(desiredPlatformVersion);
return versionA === versionB;
} catch {
return d.os_version === desiredPlatformVersion;
}
});
}
// 4) Resolve version
const versions = [...new Set(nameMatches.map((d) => d.os_version))];
const version = pickVersion(versions, desiredPlatformVersion);

/**
* Validates the selected device and handles multiple matches.
* @param matches - The list of device matches.
* @param desiredPhone - The desired phone name.
* @param desiredPlatform - The desired platform (android or ios).
* @param desiredPlatformVersion - The desired platform version.
* @returns The selected device entry.
*/
function validateAndSelectDevice(
matches: DeviceEntry[],
desiredPhone: string,
desiredPlatform: string,
desiredPlatformVersion: string,
): DeviceEntry {
if (matches.length === 0) {
// 5) Final candidates for version
const final = nameMatches.filter((d) => d.os_version === version);
if (!final.length) {
throw new Error(
`No devices found matching "${desiredPhone}" for ${desiredPlatform} ${desiredPlatformVersion}`,
`No devices for version "${version}" on ${desiredPlatform}`,
);
}

const exactMatch = matches.find(
(d) => d.display_name.toLowerCase() === desiredPhone.toLowerCase(),
);

if (exactMatch) {
return exactMatch;
} else if (matches.length >= 1) {
const names = matches.map((d) => d.display_name).join(", ");
const error_message =
matches.length === 1
? `Alternative device found: ${names}. Would you like to use it?`
: `Multiple devices found: ${names}. Please select one.`;
throw new Error(`${error_message}`);
const selected = final[0];
let note = "";
if (
version != desiredPlatformVersion &&
desiredPlatformVersion !== "latest" &&
desiredPlatformVersion !== "oldest"
) {
note = `\n Note: The requested version "${desiredPlatformVersion}" is not available. Using "${version}" instead.`;
}

return matches[0];
}

/**
* Validates the app URL.
* @param appUrl - The app URL to validate.
* @throws Will throw an error if the app URL is not valid.
*/
function validateAppUrl(appUrl: string): void {
if (!appUrl.match("bs://")) {
throw new Error("The app path is not a valid BrowserStack app URL.");
}
}
// 6) Upload app
const { app_url } = await uploadApp(appPath);
logger.info(`App uploaded: ${app_url}`);

/**
* Constructs the launch URL for the App Live session.
* @param appUrl - The app URL.
* @param device - The selected device entry.
* @param desiredPlatform - The desired platform (android or ios).
* @param desiredPlatformVersion - The desired platform version.
* @returns The constructed launch URL.
*/
function constructLaunchUrl(
appUrl: string,
device: DeviceEntry,
desiredPlatform: string,
desiredPlatformVersion: string,
): string {
// 7) Build URL & open
const deviceParam = sanitizeUrlParam(
device.display_name.replace(/\s+/g, "+"),
selected.display_name.replace(/\s+/g, "+"),
);

const params = new URLSearchParams({
os: desiredPlatform,
os_version: desiredPlatformVersion,
app_hashed_id: appUrl.split("bs://").pop() || "",
os_version: version,
app_hashed_id: app_url.split("bs://").pop() || "",
scale_to_fit: "true",
speed: "1",
start: "true",
});
const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;

return `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
openBrowser(launchUrl);
return launchUrl + note;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/tools/applive-utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface DeviceEntry {
display_name: string;
device: string;
os: string;
os_version: string;
real_mobile: boolean;
}
15 changes: 15 additions & 0 deletions src/tools/applive-utils/version-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { resolveVersion } from "../../lib/version-resolver.js";

/**
* Resolve desired version against available list
*/
export function pickVersion(available: string[], requested: string): string {
try {
return resolveVersion(requested, available);
} catch {
const opts = available.join(", ");
throw new Error(
`Version "${requested}" not found. Available versions: ${opts}`,
);
}
}
2 changes: 1 addition & 1 deletion src/tools/live-utils/desktop-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
getDevicesAndBrowsers,
BrowserStackProducts,
} from "../../lib/device-cache.js";
import { resolveVersion } from "./version-resolver.js";
import { resolveVersion } from "../../lib/version-resolver.js";
import { customFuzzySearch } from "../../lib/fuzzy.js";
import { DesktopSearchArgs, DesktopEntry } from "./types.js";

Expand Down
2 changes: 1 addition & 1 deletion src/tools/live-utils/mobile-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
getDevicesAndBrowsers,
BrowserStackProducts,
} from "../../lib/device-cache.js";
import { resolveVersion } from "./version-resolver.js";
import { resolveVersion } from "../../lib/version-resolver.js";
import { customFuzzySearch } from "../../lib/fuzzy.js";
import { MobileSearchArgs, MobileEntry } from "./types.js";

Expand Down