Skip to content

Commit 153432e

Browse files
Feat: Implement live browser session management with desktop and mobi… (#15)
* Feat: Implement live browser session management with desktop and mobile support * Feat: Update testBrowserLiveChrome function to support desktop platform and improved response handling * Feat: Enhance session handling by adding notes for OS and browser version discrepancies * Feat: Refactor session management to support desktop and mobile filtering with improved OS and browser version handling * Feat: Consolidate device caching by merging live and app live data fetching into a single function * Feat: Rename getBrowserStackData to getDevicesAndBrowsers and update related references * Fix: Improve error message for failed BrowserStack configuration fetch
1 parent 313d3a8 commit 153432e

File tree

11 files changed

+661
-259
lines changed

11 files changed

+661
-259
lines changed

src/lib/device-cache.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import fs from "fs";
2+
import os from "os";
3+
import path from "path";
4+
5+
const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
6+
const CACHE_FILE = path.join(CACHE_DIR, "data.json");
7+
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
8+
9+
const URLS = {
10+
live: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
11+
app_live:
12+
"https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
13+
};
14+
15+
/**
16+
* Fetches and caches both BrowserStack datasets (live + app_live) with a shared TTL.
17+
*/
18+
export async function getDevicesAndBrowsers(
19+
type: "live" | "app_live",
20+
): Promise<any> {
21+
if (!fs.existsSync(CACHE_DIR)) {
22+
fs.mkdirSync(CACHE_DIR, { recursive: true });
23+
}
24+
25+
let cache: any = {};
26+
27+
if (fs.existsSync(CACHE_FILE)) {
28+
const stats = fs.statSync(CACHE_FILE);
29+
if (Date.now() - stats.mtimeMs < TTL_MS) {
30+
try {
31+
cache = JSON.parse(fs.readFileSync(CACHE_FILE, "utf8"));
32+
return cache[type];
33+
} catch (error) {
34+
console.error("Error parsing cache file:", error);
35+
// Continue with fetching fresh data
36+
}
37+
}
38+
}
39+
40+
const [liveRes, appLiveRes] = await Promise.all([
41+
fetch(URLS.live),
42+
fetch(URLS.app_live),
43+
]);
44+
45+
if (!liveRes.ok || !appLiveRes.ok) {
46+
throw new Error(
47+
`Failed to fetch configuration from BrowserStack : live=${liveRes.statusText}, app_live=${appLiveRes.statusText}`,
48+
);
49+
}
50+
51+
const [liveData, appLiveData] = await Promise.all([
52+
liveRes.json(),
53+
appLiveRes.json(),
54+
]);
55+
56+
cache = { live: liveData, app_live: appLiveData };
57+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
58+
59+
return cache[type];
60+
}

src/tools/applive-utils/device-cache.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/tools/applive-utils/start-session.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import childProcess from "child_process";
22
import logger from "../../logger";
3-
import { getAppLiveData } from "./device-cache";
3+
import { getDevicesAndBrowsers } from "../../lib/device-cache";
44
import { fuzzySearchDevices } from "./fuzzy-search";
55
import { sanitizeUrlParam } from "../../lib/utils";
66
import { uploadApp } from "./upload-app";
@@ -30,7 +30,8 @@ export async function startSession(args: StartSessionArgs): Promise<string> {
3030
const { appPath, desiredPlatform, desiredPhone } = args;
3131
let { desiredPlatformVersion } = args;
3232

33-
const data = await getAppLiveData();
33+
const data = await getDevicesAndBrowsers("app_live");
34+
3435
const allDevices: DeviceEntry[] = data.mobile.flatMap((group: any) =>
3536
group.devices.map((dev: any) => ({ ...dev, os: group.os })),
3637
);
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { getDevicesAndBrowsers } from "../../lib/device-cache";
2+
import { resolveVersion } from "./version-resolver";
3+
import { customFuzzySearch } from "../../lib/fuzzy";
4+
import { DesktopSearchArgs, DesktopEntry } from "./types";
5+
6+
export async function filterDesktop(
7+
args: DesktopSearchArgs,
8+
): Promise<DesktopEntry> {
9+
const data = await getDevicesAndBrowsers("live");
10+
const allEntries = getAllDesktopEntries(data);
11+
12+
// Filter OS
13+
const osList = filterByOS(allEntries, args.os);
14+
15+
// Filter browser
16+
const browserList = filterByBrowser(osList, args.browser, args.os);
17+
18+
// Resolve OS version
19+
const uniqueOSVersions = getUniqueOSVersions(browserList);
20+
const chosenOS = resolveOSVersion(args.os, args.osVersion, uniqueOSVersions);
21+
22+
// Filter entries based on chosen OS version
23+
const entriesForOS = filterByOSVersion(browserList, chosenOS);
24+
25+
// Resolve browser version
26+
const browserVersions = entriesForOS.map((e) => e.browser_version);
27+
const chosenBrowserVersion = resolveVersion(
28+
args.browserVersion,
29+
browserVersions,
30+
);
31+
32+
// Find final entry
33+
const finalEntry = entriesForOS.find(
34+
(e) => e.browser_version === chosenBrowserVersion,
35+
);
36+
if (!finalEntry) {
37+
throw new Error(`No entry for browser version "${args.browserVersion}".`);
38+
}
39+
40+
// Add notes if versions were adjusted
41+
addNotes(finalEntry, args, chosenOS, chosenBrowserVersion);
42+
43+
return finalEntry;
44+
}
45+
46+
function getAllDesktopEntries(data: any): DesktopEntry[] {
47+
return data.desktop.flatMap((plat: any) =>
48+
plat.browsers.map((b: any) => ({
49+
os: plat.os,
50+
os_version: plat.os_version,
51+
browser: b.browser,
52+
browser_version: b.browser_version,
53+
})),
54+
);
55+
}
56+
57+
function filterByOS(entries: DesktopEntry[], os: string): DesktopEntry[] {
58+
const filtered = entries.filter((e) => e.os === os);
59+
if (!filtered.length) throw new Error(`No OS entries for "${os}".`);
60+
return filtered;
61+
}
62+
63+
function filterByBrowser(
64+
entries: DesktopEntry[],
65+
browser: string,
66+
os: string,
67+
): DesktopEntry[] {
68+
const filtered = entries.filter((e) => e.browser === browser);
69+
if (!filtered.length) throw new Error(`No browser "${browser}" on ${os}.`);
70+
return filtered;
71+
}
72+
73+
function getUniqueOSVersions(entries: DesktopEntry[]): string[] {
74+
return Array.from(new Set(entries.map((e) => e.os_version)));
75+
}
76+
77+
function resolveOSVersion(
78+
os: string,
79+
requestedVersion: string,
80+
availableVersions: string[],
81+
): string {
82+
if (os === "OS X") {
83+
return resolveMacOSVersion(requestedVersion, availableVersions);
84+
} else {
85+
// For Windows, use semantic versioning
86+
return resolveVersion(requestedVersion, availableVersions);
87+
}
88+
}
89+
90+
function resolveMacOSVersion(requested: string, available: string[]): string {
91+
if (requested === "latest") {
92+
return available[available.length - 1];
93+
} else if (requested === "oldest") {
94+
return available[0];
95+
} else {
96+
// Try fuzzy matching
97+
const fuzzy = customFuzzySearch(
98+
available.map((v) => ({ os_version: v })),
99+
["os_version"],
100+
requested,
101+
1,
102+
);
103+
const matched = fuzzy.length ? fuzzy[0].os_version : requested;
104+
105+
// Fallback if not valid
106+
return available.includes(matched) ? matched : available[0];
107+
}
108+
}
109+
110+
function filterByOSVersion(
111+
entries: DesktopEntry[],
112+
osVersion: string,
113+
): DesktopEntry[] {
114+
return entries.filter((e) => e.os_version === osVersion);
115+
}
116+
117+
function addNotes(
118+
entry: DesktopEntry,
119+
args: DesktopSearchArgs,
120+
resolvedOS: string,
121+
resolvedBrowser: string,
122+
): void {
123+
if (
124+
args.osVersion !== resolvedOS &&
125+
args.osVersion !== "latest" &&
126+
args.osVersion !== "oldest"
127+
) {
128+
entry.notes = `Note: OS version ${args.osVersion} was not found. Using "${resolvedOS}" instead.`;
129+
}
130+
131+
if (
132+
args.browserVersion !== resolvedBrowser &&
133+
args.browserVersion !== "latest" &&
134+
args.browserVersion !== "oldest"
135+
) {
136+
if (!entry.notes) {
137+
entry.notes = `Note: `;
138+
} else {
139+
entry.notes += ` `;
140+
}
141+
entry.notes += `Browser version ${args.browserVersion} was not found. Using "${resolvedBrowser}" instead.`;
142+
}
143+
}

src/tools/live-utils/mobile-filter.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { getDevicesAndBrowsers } from "../../lib/device-cache";
2+
import { resolveVersion } from "./version-resolver";
3+
import { customFuzzySearch } from "../../lib/fuzzy";
4+
import { MobileSearchArgs, MobileEntry } from "./types";
5+
6+
// Extract all mobile entries from the data
7+
function getAllMobileEntries(data: any): MobileEntry[] {
8+
return data.mobile.flatMap((grp: any) =>
9+
grp.devices.map((d: any) => ({
10+
os: grp.os,
11+
os_version: d.os_version,
12+
display_name: d.display_name,
13+
notes: "",
14+
})),
15+
);
16+
}
17+
18+
// Filter entries by OS
19+
function filterByOS(entries: MobileEntry[], os: string): MobileEntry[] {
20+
const candidates = entries.filter((d) => d.os === os);
21+
if (!candidates.length) throw new Error(`No mobile OS entries for "${os}".`);
22+
return candidates;
23+
}
24+
25+
// Find matching device with exact match validation
26+
function findMatchingDevice(
27+
entries: MobileEntry[],
28+
deviceName: string,
29+
os: string,
30+
): MobileEntry[] {
31+
const matches = customFuzzySearch(entries, ["display_name"], deviceName, 5);
32+
if (!matches.length)
33+
throw new Error(`No devices matching "${deviceName}" on ${os}.`);
34+
35+
const exact = matches.find(
36+
(m) => m.display_name.toLowerCase() === deviceName.toLowerCase(),
37+
);
38+
if (!exact) {
39+
const names = matches.map((m) => m.display_name).join(", ");
40+
throw new Error(
41+
`Alternative Device/Device's found : ${names}. Please Select one.`,
42+
);
43+
}
44+
45+
const result = entries.filter((d) => d.display_name === exact.display_name);
46+
if (!result.length)
47+
throw new Error(`No device "${exact.display_name}" on ${os}.`);
48+
49+
return result;
50+
}
51+
52+
// Find the appropriate OS version
53+
function findOSVersion(
54+
entries: MobileEntry[],
55+
requestedVersion: string,
56+
): { entries: MobileEntry[]; chosenVersion: string } {
57+
const versions = entries.map((d) => d.os_version);
58+
const chosenVersion = resolveVersion(requestedVersion, versions);
59+
60+
const result = entries.filter((d) => d.os_version === chosenVersion);
61+
if (!result.length)
62+
throw new Error(`No entry for OS version "${requestedVersion}".`);
63+
64+
return { entries: result, chosenVersion };
65+
}
66+
67+
// Create version note if needed
68+
function createVersionNote(
69+
requestedVersion: string,
70+
actualVersion: string,
71+
): string {
72+
if (
73+
actualVersion !== requestedVersion &&
74+
requestedVersion !== "latest" &&
75+
requestedVersion !== "oldest"
76+
) {
77+
return `Note: Os version ${requestedVersion} was not found. Using ${actualVersion} instead.`;
78+
}
79+
return "";
80+
}
81+
82+
export async function filterMobile(
83+
args: MobileSearchArgs,
84+
): Promise<MobileEntry> {
85+
const data = await getDevicesAndBrowsers("live");
86+
const allEntries = getAllMobileEntries(data);
87+
88+
const osCandidates = filterByOS(allEntries, args.os);
89+
const deviceCandidates = findMatchingDevice(
90+
osCandidates,
91+
args.device,
92+
args.os,
93+
);
94+
95+
const { entries: versionCandidates, chosenVersion } = findOSVersion(
96+
deviceCandidates,
97+
args.osVersion,
98+
);
99+
100+
const final = versionCandidates[0];
101+
final.notes = createVersionNote(args.osVersion, chosenVersion);
102+
103+
return final;
104+
}

0 commit comments

Comments
 (0)