Skip to content

feat(AL-13652): upgrade Appium Inspector to v2026.2.1 with AppLive customizations#19

Open
mayank2498 wants to merge 22 commits intomainfrom
AL-13652-appium-inspector-upgrade
Open

feat(AL-13652): upgrade Appium Inspector to v2026.2.1 with AppLive customizations#19
mayank2498 wants to merge 22 commits intomainfrom
AL-13652-appium-inspector-upgrade

Conversation

@mayank2498
Copy link
Copy Markdown
Collaborator

Summary

  • Upgrades Appium Inspector to v2026.2.1 (React 19, Vite 6, antd 6)
  • Re-implements 7 AppLive behavioral customizations on the new version:
    • Default tab set to Attach to Session; server tabs filtered to Remote only
    • URL param pre-fill for host/port/path from query string; window.AppLiveSessionId assignment
    • Auto-attach to session when exactly one running Appium session is found
    • PostMessage instrumentation events for tab switches, element search, tap, element select, API method calls, manual refresh, auto-reload toggle, home button, and quit session
    • Auto-reload toggle + manual refresh button in header toolbar with triggerAutoRefresh listener
    • Quit session postMessage to notify AppLive parent page
  • Restricts i18n to English only
  • Pins Node ≥ 23 (required by Vite 6)
  • Reduces body min-width from 870px → 770px

Test plan

  • Run npm run build:browser:url with PUBLIC_URL=/appium-inspector/assets/v5/ — verify dist-browser/index.html asset paths
  • Upload dist-browser/ to staging S3 and validate on real iOS device (Appium 1.21.0 compatibility)
  • Verify auto-attach fires when session is active
  • Verify postMessage events reach browserstack-fe InspectorTabs listener
  • Verify manual refresh and auto-reload toggle work in header
  • All unit tests pass: npm run test:unit

Jira: https://browserstack.atlassian.net/browse/AL-13652

🤖 Generated with Claude Code

mayank2498 and others added 17 commits April 28, 2026 15:57
… HeaderButtons (U6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fetchAllSessions merges Appium 3+, Appium 1-2, and Selenium Grid endpoints,
so the same session appears multiple times on Appium 1.x servers, causing
runningAppiumSessions.length to be > 1 and blocking auto-attach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detaching inside an AppLive iframe leaves the inspector broken with no
way to re-attach. Guard btnDetach with !window.AppLiveSessionId so it
only renders in standalone (non-AppLive) usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
localStorage persists server params across AppLive sessions. When a new
session loads with different credentials, getSetting() returns the old
session's remotePath/sessionId causing getRunningSessions() to 404.

Overwrite localStorage with current URL params at module load whenever
all AppLive params are present, so each session starts fresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pLive

BrowserStack's devtools proxy supports the legacy /appium/device/press_keycode
endpoint but not mobile:pressKey via execute/sync. Use appiumPressKeyCode in
AppLive context so Back/Home/AppSwitch buttons work via the proxy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Back/Home/AppSwitch buttons used mobile:pressKey (Appium 2.x only) — switched
to pressKeyCode in AppLive context. App ID detection used mobile:getCurrentPackage
(Appium 2.x only) — switched to getCurrentPackage in AppLive context. Both fall
back to the upstream mobile: commands outside AppLive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this, Vite outputs absolute /assets/... paths which are not
reachable through the nginx /appium-inspector/ proxy location.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap localStorage.setItem in try/catch so Safari private browsing
  (QuotaExceededError) does not crash app load on the AppLive embed path
- Add null/type guard on event.data before accessing .type in
  windowMessageCallback to prevent TypeError from non-object messages
  sent by browser extensions or React DevTools

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
} = props;
const {t} = useTranslation();

const isAutoReloadEnabledRef = useRef(isAutoReloadEnabled);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ref is initialized from the prop once but never synced back if isAutoReloadEnabled changes from outside (e.g. session reset or component remount). The windowMessageCallback reads isAutoReloadEnabledRef.current to gate triggerAutoRefresh, so a stale ref here means auto-refresh could keep firing even after it's been disabled externally.

Could you add a useEffect to keep it in sync?

useEffect(() => {
  isAutoReloadEnabledRef.current = isAutoReloadEnabled;
}, [isAutoReloadEnabled]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added a useEffect to keep isAutoReloadEnabledRef in sync with the isAutoReloadEnabled prop.

await applyAction(dispatch, getState);
}
dispatch({type: QUIT_SESSION_DONE});
window.parent.postMessage(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quitSession postMessage fires here regardless of whether this was a user-initiated quit or an unexpected server termination — manualQuit is false in the latter case but the message sent to AppLive looks identical either way.

On the bsfe side, the quitSession listener collapses the inspector panel and resets isAppiumInspectorInitiated. If a session gets killed by the server unexpectedly, the user will see the panel disappear silently with no indication of what happened.

Worth differentiating — something like passing manualQuit in the payload so AppLive can show an error state instead of silently closing:

window.parent.postMessage(
  {type: NORMAL_WINDOW_MESSAGE_EVENT, data: 'quitSession', manualQuit, sessionId: window.AppLiveSessionId},
  WINDOW_MESSAGE_TARGET_ORIGIN,
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — manualQuit is now included in the postMessage payload:

{type: NORMAL_WINDOW_MESSAGE_EVENT, data: 'quitSession', manualQuit, sessionId: window.AppLiveSessionId}

This lets the AppLive parent distinguish an unexpected server termination (manualQuit: false) from a user-initiated quit (manualQuit: true) and show an appropriate error state rather than silently collapsing the panel. The bsfe listener will need a corresponding update to act on this field.

export const NORMAL_WINDOW_MESSAGE_EVENT = 'AppLiveAppiumInspector';
export const INSTRUMENTATION_WINDOW_MESSAGE_EVENT = 'InteractionWithAppiumInspecor';
export const API_METHOD_INSTRUMENTATION_WINDOW_MESSAGE_EVENT = `${INSTRUMENTATION_WINDOW_MESSAGE_EVENT}:apiMethod`;
export const WINDOW_MESSAGE_TARGET_ORIGIN = '*';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using '*' as the target origin means all these postMessage events — which include session IDs, API method names, and durations — get broadcast to any parent frame, not just AppLive. In our current setup that's fine since this is always embedded in a BrowserStack-controlled page, but if this ever gets used elsewhere it would leak session metadata silently.

Even if locking it down to a specific origin isn't feasible right now (e.g. because the parent origin varies across environments), can we at least add a comment here explaining why '*' is intentional? Something like:

// '*' is used because the AppLive parent origin varies across environments (prod/staging/local).
// This file is only built for the AppLive-embedded context, not the standalone inspector.
export const WINDOW_MESSAGE_TARGET_ORIGIN = '*';

That way the next person who sees this doesn't assume it's an oversight.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment explaining the intent:

// '*' is intentional: the AppLive parent origin varies across prod/staging/local environments.
// This file is only built for the AppLive-embedded context, not the standalone inspector.
export const WINDOW_MESSAGE_TARGET_ORIGIN = '*';

icon={<IconLink size={16} />}
onClick={() => openLink(LINKS.CAPS_DOCS)}
onClick={() =>
openLink('https://github.com/appium/appium-inspector/releases/tag/v2026.2.1')
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version is hardcoded in the URL string here. When we upgrade to v2026.x.x next time, this will silently keep pointing to the v2026.2.1 release page unless someone catches it in the PR.

Could you derive this from a version constant instead? Something like:

import {APPIUM_INSPECTOR_VERSION} from '../../constants/common.js';
// ...
openLink(`https://github.com/appium/appium-inspector/releases/tag/v${APPIUM_INSPECTOR_VERSION}`)

Same applies to the button label on line 228 — both would stay in sync automatically that way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — extracted to APPIUM_INSPECTOR_VERSION in constants/common.js and used it in both the URL and the button label:

// common.js
export const APPIUM_INSPECTOR_VERSION = '2026.2.1';

// SessionBuilder.jsx
openLink(`https://github.com/appium/appium-inspector/releases/tag/v${APPIUM_INSPECTOR_VERSION}`)
// ...
{`Appium Inspector v${APPIUM_INSPECTOR_VERSION}`}

Next upgrade only needs a single change in common.js.

- Sync isAutoReloadEnabledRef via useEffect so external prop changes
  (e.g. Redux reset) don't leave a stale ref gating triggerAutoRefresh
- Add manualQuit to quitSession postMessage payload so AppLive parent
  can distinguish server-killed sessions from user-initiated quits
- Add comment explaining why WINDOW_MESSAGE_TARGET_ORIGIN is '*'
- Extract APPIUM_INSPECTOR_VERSION constant to avoid hardcoded version
  strings in SessionBuilder URL and label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@siddhesh-browserstack siddhesh-browserstack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four feedback items addressed correctly — the useEffect ref sync, manualQuit in the quit payload, the '*' origin comment, and the APPIUM_INSPECTOR_VERSION constant. LGTM!

mayank2498 and others added 4 commits May 1, 2026 19:32
When the OS has dark mode enabled, the inspector would inherit
`prefers-color-scheme: dark` and render in dark mode inside the AppLive
iframe. Guard `isDarkTheme` with `checkIfAllParamsPresent()` so the
AppLive context always uses the light theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Required for `npm run build` (no :url suffix) to produce correct asset
paths when serving the build for k8s regression testing via CDP
interception. The :url build overrides this via --base $PUBLIC_URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants