Skip to content
Open
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
18 changes: 18 additions & 0 deletions src/channelHandlers/browserstack-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,21 @@ export const getHarLogs = async (harLogsUrl: string) => {
return 'Failed to load network logs';
}
}

export const getScannerSessionIds = async (thBuildId: string) => {
const apiUrl = `https://scanner.browserstack.com/api/v1/debug/aut_session_ids?th_build_uuid=${thBuildId}`

const response = await fetch(apiUrl, {
method: 'GET',
headers: {
"Authorization": getAuth(),
'Content-Type': 'application/json'
}
})

if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
}

return response.json()
}
3 changes: 2 additions & 1 deletion src/constants/ipc-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const CHANNELS = {
GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS:'GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS',
GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS:'GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS',
GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS: 'GET /automate/sessions/seleniumLogs',
GET_BROWSERSTACK_AUTOMATE_HAR_LOGS: 'GET /automate/sessions/harLogs'
GET_BROWSERSTACK_AUTOMATE_HAR_LOGS: 'GET /automate/sessions/harLogs',
GET_BROWSERSTACK_SCANNER_AUTOMATE_SESSION_IDS: 'GET_BROWSERSTACK_SCANNER_AUTOMATE_SESSION_IDS'
}

export default CHANNELS
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare global {
getAutomateParsedTextLogs: (session:AutomateSessionResponse) => Promise<ParsedTextLogsResult>
getSeleniumLogs: (selenium_logs_url: string) => Promise<string>
getHarLogs: (harLogsUrl: string) => Promise<string>
getScannerSessionIds: (thBuildId: string) => Promise<any>
}

type ElectronAPI = {
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import StorageKeys from './constants/storage-keys';
import CONFIG from './constants/config';

import { mkdirSync } from 'fs'
import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs,getSeleniumLogs, getHarLogs } from './channelHandlers/browserstack-api';
import { executeCommand, getAutomateSessionDetails, getParsedAutomateTextLogs, startBrowserStackSession, stopBrowserStackSession, getAutomateParsedSeleniumLogs, getAutomateParsedSessionLogs,getSeleniumLogs, getHarLogs, getScannerSessionIds } from './channelHandlers/browserstack-api';
import { openExternalUrl } from './channelHandlers/electron-api';


Expand Down Expand Up @@ -103,6 +103,7 @@ app.whenReady().then(() => {
ipcMain.handle(CHANNELS.ELECTRON_OPEN_URL, (_, url) => openExternalUrl(url))
ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS,(_, selenium_logs_url) => getSeleniumLogs(selenium_logs_url));
ipcMain.handle(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, (_, har_logs_url) => getHarLogs(har_logs_url));
ipcMain.handle(CHANNELS.GET_BROWSERSTACK_SCANNER_AUTOMATE_SESSION_IDS, (_, thBuildId) => getScannerSessionIds(thBuildId));
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.
3 changes: 2 additions & 1 deletion src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const browserstackAPI: BrowserStackAPI = {
getAutomateParsedSessionLogs: (session)=>ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_SESSION_LOGS,session),
getAutomateParsedSeleniumLogs: (session)=>ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_PARSED_SELENIUM_LOGS,session),
getSeleniumLogs: (selenium_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_SELENIUM_LOGS, selenium_logs_url),
getHarLogs: (har_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, har_logs_url)
getHarLogs: (har_logs_url) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_AUTOMATE_HAR_LOGS, har_logs_url),
getScannerSessionIds: (thBuildId: string) => ipcRenderer.invoke(CHANNELS.GET_BROWSERSTACK_SCANNER_AUTOMATE_SESSION_IDS, thBuildId)
}

const electronAPI: ElectronAPI = {
Expand Down
10 changes: 6 additions & 4 deletions src/renderer/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import ReplayTool from "./routes/automate/tools/replay-tool";
import LatencyFinder from "./routes/automate/tools/latency-finder";
import SessionComparison from "./routes/automate/tools/session-comparison";
import AppAutomatePage from "./routes/app-automate";
import SessionFinder from "./routes/website-scanner/tools/session-finder";
import WebsiteScannerPage from "./routes/website-scanner";

const Products = [
{
Expand Down Expand Up @@ -91,14 +93,14 @@ const Products = [
},
{
name: "Web Accessibility",
path: "/web-accessibility",
page: AutomatePage,
path: "/website-scanner",
page: WebsiteScannerPage,
tools: [
{
title: "Automate Session Finder",
description: "Find associated automate session for accessibility scanner run",
path: "/web-a11y/session-finder",
component: null,
path: "/website-scanner/session-finder",
component: SessionFinder,
},
],
},
Expand Down
43 changes: 43 additions & 0 deletions src/renderer/routes/website-scanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NavLink } from "react-router-dom"

export default function WebsiteScannerPage(props: ProductPageProps) {
const { tools } = props

return (
<div className="p-5">
<div className="grid grid-col-3 lg:grid-cols-4 gap-4">
{tools.map((tool) => {
const isComingSoon = tool.component === null

const Card = (
<div className="card bg-base-100 w-full h-full shadow-sm border">
<div className="card-body">
<h2 className="card-title flex items-center gap-2">
{tool.title}
</h2>
<p>{tool.description}</p>
<div>
{isComingSoon && (
<span className="badge badge-warning badge-sm">
Coming soon
</span>
)}
</div>
</div>
</div>
)

return isComingSoon ? (
<div key={tool.path} className="cursor-not-allowed opacity-60">
{Card}
</div>
) : (
<NavLink key={tool.path} to={tool.path}>
{Card}
</NavLink>
)
})}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './session-finder/index'
139 changes: 139 additions & 0 deletions src/renderer/routes/website-scanner/tools/session-finder/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useState } from 'react'
import { toast } from 'react-toastify'

// Function to extract thBuildId from various URL formats
const extractThBuildId = (url: string): string | null => {
try {
const urlObj = new URL(url)

// Case 1: Direct thBuildId in query params
const thBuildId = urlObj.searchParams.get('thBuildId')
if (thBuildId) {
return thBuildId
}

// Case 2: thBuildId in reportUrl parameter (nested URL)
const reportUrl = urlObj.searchParams.get('reportUrl')
if (reportUrl) {
const reportUrlObj = new URL(decodeURIComponent(reportUrl))
const nestedThBuildId = reportUrlObj.searchParams.get('thBuildId')
if (nestedThBuildId) {
return nestedThBuildId
}
}

return null
} catch (error) {
console.error('Error parsing URL:', error)
return null
}
}

export default function SessionFinder() {
const [url, setUrl] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [sessionData, setSessionData] = useState<any>(null)

const handleFindSession = async () => {
if (!url.trim()) {
toast.error('Please enter a Website Scanner Accessibility Report URL')
return
}

setIsLoading(true)
try {
// Extract thBuildId from the URL
const thBuildId = extractThBuildId(url)
if (!thBuildId) {
toast.error('Could not find thBuildId in the provided URL')
return
}

let credentials = await window.credentialsAPI.getBrowserStackAdminCredentials()
if (!credentials) {
credentials = await window.credentialsAPI.getBrowserStackDemoCredentials()
}

if (!credentials) {
toast.error('Please configure your BrowserStack credentials first')
return
}

const result = await window.browserstackAPI.getScannerSessionIds(thBuildId)
setSessionData(result)
} catch (error) {
console.error('Error finding session:', error)
toast.error(error instanceof Error ? error.message : 'Failed to find session. Please check the URL and try again.')
} finally {
setIsLoading(false)
}
}

return (
<div className="p-6 rounded-box">
<h2 className="text-lg font-bold mb-4">Session Finder</h2>
<div className="flex gap-2">
<div className="form-control flex-1">
<label className="label">
<span className="label-text">Website Scanner Accessibility Report URL</span>
</label>
<input
type="url"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="input input-bordered w-full placeholder-gray-300"
placeholder="https://scanner.browserstack.com/site-scanner/..."
/>
</div>
<div className="flex items-end">
<button
onClick={handleFindSession}
disabled={isLoading}
className="btn btn-neutral"
>
{isLoading ? (
<span className="loading loading-spinner"></span>
) : (
'Find Session'
)}
</button>
</div>
</div>

{sessionData && (
<div className="mt-6">
<h3 className="text-md font-semibold mb-3">Session Results</h3>
<div className="bg-base-200 rounded-lg p-4">

{sessionData.data && Object.keys(sessionData.data as Record<string, string[]>).length > 0 && (
<div className="mt-4">
<h4 className="font-medium mb-2">Automate Session:</h4>
<div className="space-y-2 max-h-60 overflow-y-auto">
{Object.entries(sessionData.data as Record<string, string[]>).map(([thBuildId, sessionArray]: [string, string[]]) => (
<div key={thBuildId} className="mb-4">
<div className="text-sm font-medium mb-2 opacity-70">
TestHub Build ID: {thBuildId}
</div>
{Array.isArray(sessionArray) && sessionArray.map((sessionId: string, index: number) => (
<div
key={index}
onClick={() => window.electronAPI.openExternalUrl(`https://automate.browserstack.com/dashboard/v2/sessions/${sessionId}`)}
className="block bg-base-100 rounded p-3 text-sm font-mono hover:bg-base-300 transition-colors cursor-pointer"
>
<div className="flex items-center justify-between">
<span>Automate Session ID: {sessionId}</span>
<span className="text-xs opacity-60">Open in Browser →</span>
</div>
</div>
))}
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}