diff --git a/src/client/power-pages/actions-hub/handlers/PreviewSiteHandler.ts b/src/client/power-pages/actions-hub/handlers/PreviewSiteHandler.ts index 24b910c5f..af9c990e0 100644 --- a/src/client/power-pages/actions-hub/handlers/PreviewSiteHandler.ts +++ b/src/client/power-pages/actions-hub/handlers/PreviewSiteHandler.ts @@ -13,7 +13,7 @@ export const previewSite = async (siteTreeItem: SiteTreeItem) => { try { await PreviewSite.clearCache(siteTreeItem.siteInfo.websiteUrl); - await PreviewSite.launchBrowserAndDevToolsWithinVsCode(siteTreeItem.siteInfo.websiteUrl, siteTreeItem.siteInfo.dataModelVersion, siteTreeItem.siteInfo.siteVisibility); + await PreviewSite.launchSitePreviewWithinVsCode(siteTreeItem.siteInfo.websiteUrl, siteTreeItem.siteInfo.dataModelVersion, siteTreeItem.siteInfo.siteVisibility); } catch (error) { traceError(Constants.EventNames.ACTIONS_HUB_PREVIEW_SITE_FAILED, error as Error, { methodName: previewSite.name }); } diff --git a/src/client/power-pages/preview-site/PreviewSite.ts b/src/client/power-pages/preview-site/PreviewSite.ts index d098a6ca4..ad7694fcd 100644 --- a/src/client/power-pages/preview-site/PreviewSite.ts +++ b/src/client/power-pages/preview-site/PreviewSite.ts @@ -26,6 +26,8 @@ import { SiteVisibility } from '../actions-hub/models/SiteVisibility'; import { uploadSite } from '../actions-hub/handlers/UploadSiteHandler'; export const SITE_PREVIEW_COMMAND_ID = "microsoft.powerplatform.pages.preview-site"; +const INTEGRATED_BROWSER_COMMAND_ID = 'workbench.action.browser.open'; +const EDGE_DEVTOOLS_COMMAND_ID = 'vscode-edge-devtools.launch'; export class PreviewSite { private static _websiteDetails: IWebsiteDetails | undefined = undefined; @@ -126,15 +128,13 @@ export class PreviewSite { } } - public static async launchBrowserAndDevToolsWithinVsCode(webSitePreviewURL: string | undefined, dataModelVersion: 1 | 2, siteVisibility: SiteVisibility | undefined): Promise { + public static async launchSitePreviewWithinVsCode(webSitePreviewURL: string | undefined, dataModelVersion: 1 | 2, siteVisibility: SiteVisibility | undefined): Promise { if (!webSitePreviewURL || webSitePreviewURL === "" || !siteVisibility) { return; } - const edgeToolsExtension = vscode.extensions.getExtension(EDGE_TOOLS_EXTENSION_ID); - - if (!edgeToolsExtension) { - await PreviewSite.promptInstallEdgeTools(); + const previewBrowser = await PreviewSite.getSitePreviewBrowser(); + if (!previewBrowser) { return; } @@ -142,8 +142,7 @@ export class PreviewSite { Messages.OPENING_SITE_PREVIEW, false, async () => { - PreviewSite.closeExistingPreview(); - await vscode.commands.executeCommand('vscode-edge-devtools.launch', { launchUrl: webSitePreviewURL }); + await PreviewSite.openSitePreviewBrowser(webSitePreviewURL, previewBrowser); } ); @@ -153,6 +152,37 @@ export class PreviewSite { } } + private static async getSitePreviewBrowser(): Promise<'integratedBrowser' | 'edgeTools' | undefined> { + const commands = await vscode.commands.getCommands(true); + + // VS Code's integrated browser is available through a workbench command in + // newer builds. Older builds fall back to the existing Edge Tools path because + // Simple Browser does not fully render Power Pages sites. + // See https://github.com/microsoft/vscode/blob/main/extensions/simple-browser/src/extension.ts + if (commands.includes(INTEGRATED_BROWSER_COMMAND_ID)) { + return 'integratedBrowser'; + } + + const edgeToolsExtension = vscode.extensions.getExtension(EDGE_TOOLS_EXTENSION_ID); + + if (!edgeToolsExtension) { + await PreviewSite.promptInstallEdgeTools(); + return undefined; + } + + return 'edgeTools'; + } + + private static async openSitePreviewBrowser(webSitePreviewURL: string, previewBrowser: 'integratedBrowser' | 'edgeTools'): Promise { + if (previewBrowser === 'integratedBrowser') { + await vscode.commands.executeCommand(INTEGRATED_BROWSER_COMMAND_ID, webSitePreviewURL); + return; + } + + PreviewSite.closeExistingPreview(); + await vscode.commands.executeCommand(EDGE_DEVTOOLS_COMMAND_ID, { launchUrl: webSitePreviewURL }); + } + private static async showUploadWarning(websitePath: string, dataModelVersion: 1 | 2, siteVisibility: SiteVisibility) { const pendingChangesResult = await PreviewSite._pacTerminal.getWrapper().pendingChanges(websitePath, dataModelVersion); @@ -219,7 +249,7 @@ export class PreviewSite { await PreviewSite.clearCache(PreviewSite._websiteDetails.websiteUrl); - await PreviewSite.launchBrowserAndDevToolsWithinVsCode( + await PreviewSite.launchSitePreviewWithinVsCode( PreviewSite._websiteDetails.websiteUrl, PreviewSite._websiteDetails.dataModel === WebsiteDataModel.Standard ? 1 : 2, PreviewSite._websiteDetails.siteVisibility diff --git a/src/client/test/integration/power-pages/actions-hub/handlers/PreviewSiteHandler.test.ts b/src/client/test/integration/power-pages/actions-hub/handlers/PreviewSiteHandler.test.ts index fa94100ce..768909383 100644 --- a/src/client/test/integration/power-pages/actions-hub/handlers/PreviewSiteHandler.test.ts +++ b/src/client/test/integration/power-pages/actions-hub/handlers/PreviewSiteHandler.test.ts @@ -32,12 +32,12 @@ describe('PreviewSiteHandler', () => { describe('previewSite', () => { let mockPreviewSiteClearCache: sinon.SinonStub; - let mockLaunchBrowserAndDevTools: sinon.SinonStub; + let mockLaunchSitePreview: sinon.SinonStub; let mockSiteInfo: IWebsiteInfo; beforeEach(() => { mockPreviewSiteClearCache = sandbox.stub(PreviewSite, 'clearCache'); - mockLaunchBrowserAndDevTools = sandbox.stub(PreviewSite, 'launchBrowserAndDevToolsWithinVsCode'); + mockLaunchSitePreview = sandbox.stub(PreviewSite, 'launchSitePreviewWithinVsCode'); mockSiteInfo = { name: "Test Site", websiteId: "test-id", @@ -53,13 +53,13 @@ describe('PreviewSiteHandler', () => { }; }); - it('should clear cache and launch browser with dev tools', async () => { + it('should clear cache and launch site preview', async () => { const siteTreeItem = new SiteTreeItem(mockSiteInfo); await previewSite(siteTreeItem); expect(mockPreviewSiteClearCache.calledOnceWith('https://test-site.com')).to.be.true; - expect(mockLaunchBrowserAndDevTools.calledOnceWith('https://test-site.com', 1)).to.be.true; + expect(mockLaunchSitePreview.calledOnceWith('https://test-site.com', 1)).to.be.true; }); }); }); diff --git a/src/client/test/integration/power-pages/preview-site/PreviewSite.test.ts b/src/client/test/integration/power-pages/preview-site/PreviewSite.test.ts index b6ace7403..44797dda3 100644 --- a/src/client/test/integration/power-pages/preview-site/PreviewSite.test.ts +++ b/src/client/test/integration/power-pages/preview-site/PreviewSite.test.ts @@ -13,6 +13,7 @@ import { Events } from '../../../../power-pages/preview-site/Constants'; import ArtemisContext from '../../../../ArtemisContext'; import PacContext from '../../../../pac/PacContext'; import * as sinon from 'sinon'; +import { SiteVisibility } from '../../../../power-pages/actions-hub/models/SiteVisibility'; describe('PreviewSite', () => { let sandbox: sinon.SinonSandbox; @@ -182,4 +183,44 @@ describe('PreviewSite', () => { expect(getWebSiteUrlStub.calledOnceWith(mockWorkspaceFolders)).to.be.true; }); }); + + describe('launchSitePreviewWithinVsCode', () => { + let executeCommandStub: sinon.SinonStub; + + beforeEach(() => { + executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand'); + sandbox.stub(vscode.window, 'withProgress').callsFake(async (_options, task) => { + return await task({ report: sandbox.stub() }, {} as vscode.CancellationToken); + }); + sandbox.stub(PreviewSite as unknown as { showUploadWarning: () => Promise }, 'showUploadWarning').resolves(); + }); + + it('should open the integrated browser when the VS Code command is available', async () => { + sandbox.stub(vscode.commands, 'getCommands').resolves(['workbench.action.browser.open']); + + await PreviewSite.launchSitePreviewWithinVsCode('https://test-site.com', 1, SiteVisibility.Public); + + expect(executeCommandStub.calledOnceWith('workbench.action.browser.open', 'https://test-site.com')).to.be.true; + }); + + it('should fall back to Edge Tools when the integrated browser command is not available', async () => { + sandbox.stub(vscode.commands, 'getCommands').resolves(['vscode-edge-devtools.launch']); + sandbox.stub(vscode.extensions, 'getExtension').withArgs('ms-edgedevtools.vscode-edge-devtools').returns({} as vscode.Extension); + + await PreviewSite.launchSitePreviewWithinVsCode('https://test-site.com', 1, SiteVisibility.Public); + + expect(executeCommandStub.calledOnceWith('vscode-edge-devtools.launch', { launchUrl: 'https://test-site.com' })).to.be.true; + }); + + it('should prompt to install Edge Tools when no preview browser is available', async () => { + sandbox.stub(vscode.commands, 'getCommands').resolves([]); + sandbox.stub(vscode.extensions, 'getExtension').withArgs('ms-edgedevtools.vscode-edge-devtools').returns(undefined); + const showWarningMessageStub = sandbox.stub(vscode.window, 'showWarningMessage'); + + await PreviewSite.launchSitePreviewWithinVsCode('https://test-site.com', 1, SiteVisibility.Public); + + expect(showWarningMessageStub.calledOnce).to.be.true; + expect(executeCommandStub.called).to.be.false; + }); + }); }); diff --git a/src/client/test/runTest.ts b/src/client/test/runTest.ts index 9922318cd..5875aab7c 100644 --- a/src/client/test/runTest.ts +++ b/src/client/test/runTest.ts @@ -4,10 +4,17 @@ */ import * as path from "path"; +import * as fs from "fs"; +import * as os from "os"; import { runTests } from "@vscode/test-electron"; async function main() { + // VS Code creates IPC sockets under the user-data directory. Keeping that + // directory in the system temp path avoids macOS Unix socket path limits when + // tests run from long worktree paths. + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "pp-vscode-client-")); + try { // The folder containing the Extension Manifest package.json // Passed to `--extensionDevelopmentPath` @@ -24,11 +31,13 @@ async function main() { // version: 'insiders', extensionDevelopmentPath, extensionTestsPath, - launchArgs: ['--no-sandbox', '--disable-gpu'] + launchArgs: ['--no-sandbox', '--disable-gpu', `--user-data-dir=${userDataDir}`] }); } catch (err) { console.error("Failed to run tests"); - process.exit(1); + throw err; + } finally { + fs.rmSync(userDataDir, { recursive: true, force: true }); } }