diff --git a/src/extension/android/androidPlatform.ts b/src/extension/android/androidPlatform.ts index cc93fb42a..7aac81529 100644 --- a/src/extension/android/androidPlatform.ts +++ b/src/extension/android/androidPlatform.ts @@ -208,6 +208,40 @@ export class AndroidPlatform extends GeneralMobilePlatform { targetId = await this.getTargetIdForRunApp(onlineTargetsIds); this.packageName = await this.getPackageName(); devicesIdsForLaunch = [targetId]; + + // Save target info for status indicator + if (targetId) { + const onlineTargets = await this.adbHelper.getOnlineTargets(); + const targetInfo = onlineTargets.find(t => t.id === targetId); + if (targetInfo) { + let deviceName = targetId; + try { + // For emulators, try to get AVD name first + if (targetInfo.isVirtualTarget) { + const avdName = await this.adbHelper.getAvdNameById(targetId); + if (avdName) { + deviceName = `${avdName} (${targetId})`; + } + } else { + // For physical devices, get model name + const modelResult = await this.adbHelper.executeQuery( + targetId, + "shell getprop ro.product.model", + ); + const model = modelResult.trim(); + if (model) { + deviceName = `${model} (${targetId})`; + } + } + } catch (error) {} + this.target = new AndroidTarget( + targetInfo.isOnline, + targetInfo.isVirtualTarget, + targetInfo.id, + deviceName, + ); + } + } } } catch (error) { if (!targetId) { diff --git a/src/extension/appLauncher.ts b/src/extension/appLauncher.ts index 2232e2d3d..710cb1917 100644 --- a/src/extension/appLauncher.ts +++ b/src/extension/appLauncher.ts @@ -304,7 +304,9 @@ export class AppLauncher { try { if (this.mobilePlatform instanceof GeneralMobilePlatform) { generator.step("resolveMobileTarget"); - await this.resolveAndSaveMobileTarget(launchArgs, this.mobilePlatform); + if (launchArgs.target) { + await this.resolveAndSaveMobileTarget(launchArgs, this.mobilePlatform); + } } await this.mobilePlatform.beforeStartPackager(); @@ -339,6 +341,14 @@ export class AppLauncher { await this.mobilePlatform.runApp(); } + // Show device name in status bar after app is launched + if (this.mobilePlatform instanceof GeneralMobilePlatform) { + const target = this.mobilePlatform.getResolvedTarget(); + if (target?.name) { + DeviceStatusIndicator.show(target.name); + } + } + if (mobilePlatformOptions.isDirect) { if (launchArgs.useHermesEngine) { generator.step("mobilePlatform.enableHermesDebuggingMode"); @@ -598,11 +608,6 @@ export class AppLauncher { }); } } - - const target = mobilePlatform.getResolvedTarget(); - if (target?.name) { - DeviceStatusIndicator.show(target.name); - } } } diff --git a/src/extension/ios/iOSPlatform.ts b/src/extension/ios/iOSPlatform.ts index 91c16a4c8..c8afc384a 100644 --- a/src/extension/ios/iOSPlatform.ts +++ b/src/extension/ios/iOSPlatform.ts @@ -224,6 +224,11 @@ export class IOSPlatform extends GeneralMobilePlatform { () => Promise.resolve(IOSPlatform.RUN_IOS_FAILURE_PATTERNS), PlatformType.iOS, ).process(runIosSpawn); + + // Ensure target is set for status indicator + if (!this.target) { + await this.getTarget(); + } }); } diff --git a/test/extension/deviceStatusIndicator.test.ts b/test/extension/deviceStatusIndicator.test.ts new file mode 100644 index 000000000..5947a45c4 --- /dev/null +++ b/test/extension/deviceStatusIndicator.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import * as vscode from "vscode"; +import * as sinon from "sinon"; +import { DeviceStatusIndicator } from "../../src/extension/deviceStatusIndicator"; + +function makeFakeStatusBarItem(): vscode.StatusBarItem { + return { + id: "", + name: "", + text: "", + tooltip: undefined, + color: undefined, + backgroundColor: undefined, + command: undefined, + accessibilityInformation: undefined, + alignment: vscode.StatusBarAlignment.Left, + priority: undefined, + show: sinon.stub(), + hide: sinon.stub(), + dispose: sinon.stub(), + } as unknown as vscode.StatusBarItem; +} + +suite("DeviceStatusIndicator", function () { + suite("extensionContext", function () { + let createStatusBarItemStub: Sinon.SinonStub; + let fakeItem: vscode.StatusBarItem; + setup(() => { + fakeItem = makeFakeStatusBarItem(); + createStatusBarItemStub = sinon.stub(vscode.window, "createStatusBarItem"); + createStatusBarItemStub.returns(fakeItem); + }); + teardown(() => { + const instance = (DeviceStatusIndicator as any).instance; + if (instance) { + instance.dispose(); + } + createStatusBarItemStub.restore(); + }); + test("show should set correct icon and device name in text", function () { + DeviceStatusIndicator.show("Test Device"); + assert.strictEqual(fakeItem.text, "$(device-mobile) Test Device"); + }); + test("show should set correct tooltip with device name", function () { + DeviceStatusIndicator.show("Test Device"); + assert.strictEqual(fakeItem.tooltip, "Active debug target: Test Device"); + }); + test("show should call show on the status bar item", function () { + DeviceStatusIndicator.show("Test Device"); + assert.ok((fakeItem.show as Sinon.SinonStub).calledOnce); + }); + test("hide should call hide on the status bar item after show", function () { + DeviceStatusIndicator.show("Test Device"); + DeviceStatusIndicator.hide(); + assert.ok((fakeItem.hide as Sinon.SinonStub).calledOnce); + }); + test("hide before show should not throw", function () { + assert.doesNotThrow(() => DeviceStatusIndicator.hide()); + }); + test("show should reuse singleton instance across multiple calls", function () { + DeviceStatusIndicator.show("Device A"); + DeviceStatusIndicator.show("Device B"); + assert.strictEqual(createStatusBarItemStub.callCount, 1); + assert.strictEqual(fakeItem.text, "$(device-mobile) Device B"); + }); + test("dispose should clear the singleton instance", function () { + DeviceStatusIndicator.show("Test Device"); + const instance = (DeviceStatusIndicator as any).instance as DeviceStatusIndicator; + instance.dispose(); + assert.strictEqual((DeviceStatusIndicator as any).instance, undefined); + }); + }); +}); diff --git a/test/extension/packagerStatusIndicator.test.ts b/test/extension/packagerStatusIndicator.test.ts new file mode 100644 index 000000000..204f6ba9c --- /dev/null +++ b/test/extension/packagerStatusIndicator.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for details. + +import assert = require("assert"); +import * as vscode from "vscode"; +import * as sinon from "sinon"; +import { + PackagerStatus, + PackagerStatusIndicator, +} from "../../src/extension/packagerStatusIndicator"; +import { SettingsHelper } from "../../src/extension/settingsHelper"; + +function makeFakeStatusBarItem(): vscode.StatusBarItem { + return { + id: "", + name: "", + text: "", + tooltip: undefined, + color: undefined, + backgroundColor: undefined, + command: undefined, + accessibilityInformation: undefined, + alignment: vscode.StatusBarAlignment.Left, + priority: undefined, + show: sinon.stub(), + hide: sinon.stub(), + dispose: sinon.stub(), + } as unknown as vscode.StatusBarItem; +} + +suite("PackagerStatusIndicator", function () { + suite("extensionContext", function () { + let createStatusBarItemStub: Sinon.SinonStub; + let getShowIndicatorStub: Sinon.SinonStub; + let getPatternStub: Sinon.SinonStub; + let fakeToggleItem: vscode.StatusBarItem; + let fakeRestartItem: vscode.StatusBarItem; + let indicator: PackagerStatusIndicator; + const PROJECT_ROOT = "/workspace"; + setup(() => { + fakeRestartItem = makeFakeStatusBarItem(); + fakeToggleItem = makeFakeStatusBarItem(); + createStatusBarItemStub = sinon.stub(vscode.window, "createStatusBarItem"); + createStatusBarItemStub.onFirstCall().returns(fakeRestartItem); + createStatusBarItemStub.onSecondCall().returns(fakeToggleItem); + getShowIndicatorStub = sinon.stub(SettingsHelper, "getShowIndicator").returns(true); + getPatternStub = sinon + .stub(SettingsHelper, "getPackagerStatusIndicatorPattern") + .returns(PackagerStatusIndicator.FULL_VERSION); + indicator = new PackagerStatusIndicator(PROJECT_ROOT); + }); + teardown(() => { + indicator.dispose(); + createStatusBarItemStub.restore(); + getShowIndicatorStub.restore(); + getPatternStub.restore(); + }); + test("should display port in full version when packager starts with port", function () { + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED, 8081); + assert.ok( + (fakeToggleItem.text as string).includes(":8081"), + `Expected text to contain ':8081', got: ${fakeToggleItem.text}`, + ); + }); + test("should not display port in full version when packager starts without port", function () { + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED); + assert.ok( + !(fakeToggleItem.text as string).includes(":"), + `Expected text to not contain port, got: ${fakeToggleItem.text}`, + ); + }); + test("should clear port from status bar text when packager stops", function () { + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED, 8081); + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STOPPED); + assert.ok( + !(fakeToggleItem.text as string).includes(":8081"), + `Expected text to not contain ':8081' after stop, got: ${fakeToggleItem.text}`, + ); + }); + test("should display port in short version when packager starts with port", function () { + getPatternStub.returns(PackagerStatusIndicator.SHORT_VERSION); + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED, 9090); + assert.ok( + (fakeToggleItem.text as string).includes(":9090"), + `Expected short version text to contain ':9090', got: ${fakeToggleItem.text}`, + ); + }); + test("should retain port across status transitions from starting to started", function () { + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTING, 8081); + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED); + assert.ok( + (fakeToggleItem.text as string).includes(":8081"), + `Expected port to be retained after STARTED, got: ${fakeToggleItem.text}`, + ); + }); + test("should update port when called with a new port value", function () { + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED, 8081); + indicator.updatePackagerStatus(PackagerStatus.PACKAGER_STARTED, 9090); + assert.ok( + (fakeToggleItem.text as string).includes(":9090"), + `Expected text to contain updated port ':9090', got: ${fakeToggleItem.text}`, + ); + assert.ok( + !(fakeToggleItem.text as string).includes(":8081"), + `Expected old port ':8081' to be gone, got: ${fakeToggleItem.text}`, + ); + }); + }); +});