diff --git a/.ci/pipeline.yml b/.ci/pipeline.yml index c79d2b51..5dbe559c 100644 --- a/.ci/pipeline.yml +++ b/.ci/pipeline.yml @@ -19,4 +19,14 @@ jobs: pool: vmImage: 'vs2017-win2016' steps: + - powershell: | + # This step can be removed once Edge is included on the test VM image. + # The download URI comes from: https://www.microsoftedgeinsider.com/en-us/download + $InstallerDownloadURI = "https://go.microsoft.com/fwlink/?linkid=2084649&Channel=Canary&language=en&Consent=1" + $InstallerExe = "$ENV:BUILD_ARTIFACTSTAGINGDIRECTORY\MicrosoftEdgeSetup.exe" + Invoke-WebRequest -Uri $InstallerDownloadURI -OutFile $InstallerExe + Start-Process $InstallerExe -Wait -ArgumentList '/silent /install' + # Cleanup installer + Remove-Item $InstallerExe + displayName: Install Edge Canary - template: common-validation.yml diff --git a/demos/webview/WebView2Sample/WebView2Sample.vcxproj b/demos/webview/WebView2Sample/WebView2Sample.vcxproj index 89a73b35..896b2b95 100644 --- a/demos/webview/WebView2Sample/WebView2Sample.vcxproj +++ b/demos/webview/WebView2Sample/WebView2Sample.vcxproj @@ -103,6 +103,7 @@ true _DEBUG;_WINDOWS;%(PreprocessorDefinitions) true + MultiThreaded true diff --git a/demos/webview/WebView2Sample/WebView2Sample.vcxproj.filters b/demos/webview/WebView2Sample/WebView2Sample.vcxproj.filters index 8e4c2d8d..fa928368 100644 --- a/demos/webview/WebView2Sample/WebView2Sample.vcxproj.filters +++ b/demos/webview/WebView2Sample/WebView2Sample.vcxproj.filters @@ -21,6 +21,5 @@ - \ No newline at end of file diff --git a/src/binder.ts b/src/binder.ts index 0b150871..3fbe312d 100644 --- a/src/binder.ts +++ b/src/binder.ts @@ -43,7 +43,7 @@ const packageJson = require('../../package.json'); export interface IBinderDelegate { acquireDap(target: ITarget): Promise; // Returns whether we should disable child session treatment. - initAdapter(debugAdapter: DebugAdapter, target: ITarget): Promise; + initAdapter(debugAdapter: DebugAdapter, target: ITarget, launcher: ILauncher): Promise; releaseDap(target: ITarget): void; } @@ -84,7 +84,7 @@ export class Binder implements IDisposable { launcher.onTargetListChanged( () => { const targets = this.targetList(); - this._attachToNewTargets(targets); + this._attachToNewTargets(targets, launcher); this._detachOrphanThreads(targets); this._onTargetListChangedEmitter.fire(); }, @@ -322,7 +322,7 @@ export class Binder implements IDisposable { return result; } - async attach(target: ITarget) { + async attach(target: ITarget, launcher: ILauncher) { if (!target.canAttach()) return; const cdp = await target.attach(); if (!cdp) return; @@ -357,7 +357,7 @@ export class Binder implements IDisposable { cdp.Runtime.runIfWaitingForDebugger({}); return {}; }; - if (await this._delegate.initAdapter(debugAdapter, target)) { + if (await this._delegate.initAdapter(debugAdapter, target, launcher)) { startThread(); } else { dap.on('attach', startThread); @@ -386,7 +386,7 @@ export class Binder implements IDisposable { this._releaseTarget(target); } - _attachToNewTargets(targets: ITarget[]) { + _attachToNewTargets(targets: ITarget[], launcher: ILauncher) { for (const target of targets.values()) { if (!target.waitingForDebugger()) { continue; @@ -394,7 +394,7 @@ export class Binder implements IDisposable { const thread = this._threads.get(target); if (!thread) { - this.attach(target); + this.attach(target, launcher); } } } diff --git a/src/test/test.ts b/src/test/test.ts index e3a2ab28..aae02101 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -19,10 +19,10 @@ import { INodeLaunchConfiguration, nodeAttachConfigDefaults, nodeLaunchConfigDefaults, + AnyChromiumLaunchConfiguration, } from '../configuration'; import Dap from '../dap/api'; import DapConnection from '../dap/connection'; -import { ChromeLauncher } from '../targets/browser/chromeLauncher'; import { ITarget } from '../targets/targets'; import { GoldenText } from './goldenText'; import { Logger } from './logger'; @@ -32,6 +32,7 @@ import { TelemetryReporter } from '../telemetry/telemetryReporter'; import { ILogger } from '../common/logging'; import { Logger as AdapterLogger } from '../common/logging/logger'; import { createTopLevelSessionContainer, createGlobalContainer } from '../ioc'; +import { BrowserLauncher } from '../targets/browser/browserLauncher'; export const kStabilizeNames = ['id', 'threadId', 'sourceReference', 'variablesReference']; @@ -112,7 +113,11 @@ export interface ITestHandle { readonly assertLog: AssertLog; load(): Promise; - _init(adapter: DebugAdapter, target: ITarget): Promise; + _init( + adapter: DebugAdapter, + target: ITarget, + launcher: BrowserLauncher, + ): Promise; } export class TestP implements ITestHandle { @@ -196,7 +201,11 @@ export class TestP implements ITestHandle { return this._root.workspacePath(relative); } - async _init(adapter: DebugAdapter) { + async _init( + adapter: DebugAdapter, + _target: ITarget, + launcher: BrowserLauncher, + ) { adapter.breakpointManager.setPredictorDisabledForTest(true); adapter.sourceContainer.setSourceMapTimeouts({ load: 0, @@ -206,7 +215,8 @@ export class TestP implements ITestHandle { }); this._adapter = adapter; - this._connection = this._root._browserLauncher.connectionForTest()!; + this._root._browserLauncher = launcher; + this._connection = this._root._browserLauncher?.connectionForTest()!; const result = await this._connection.rootSession().Target.attachToBrowserTarget({}); const testSession = this._connection.createSession(result!.sessionId); const { sessionId } = (await testSession.Target.attachToTarget({ @@ -306,7 +316,7 @@ export class TestRoot { private _workerCallback: (session: ITestHandle) => void; private _launchCallback: (session: ITestHandle) => void; - _browserLauncher: ChromeLauncher; + _browserLauncher: BrowserLauncher | undefined; readonly binder: Binder; private _onSessionCreatedEmitter = new EventEmitter(); @@ -337,7 +347,7 @@ export class TestRoot { const services = createTopLevelSessionContainer( createGlobalContainer({ storagePath, isVsCode: true }), ); - this._browserLauncher = services.get(ChromeLauncher); + this.binder = new Binder( this, this._root.adapterConnection, @@ -359,13 +369,17 @@ export class TestRoot { return p._session.adapterConnection; } - async initAdapter(adapter: DebugAdapter, target: ITarget): Promise { + async initAdapter( + adapter: DebugAdapter, + target: ITarget, + launcher: BrowserLauncher, + ): Promise { const p = this._targetToP.get(target); if (!p) { return true; } - const boot = await p._init(adapter, target); + const boot = await p._init(adapter, target, launcher); if (target.parent()) this._workerCallback(p); else this._launchCallback(p); this._onSessionCreatedEmitter.fire(p); @@ -489,7 +503,7 @@ export class TestRoot { async disconnect(): Promise { return new Promise(cb => { this.initialize.then(() => { - const connection = this._browserLauncher.connectionForTest(); + const connection = this._browserLauncher?.connectionForTest(); if (connection) { const disposable = connection.onDisconnected(() => { cb(); diff --git a/src/test/testRunner.ts b/src/test/testRunner.ts index 198a1157..580fc0b3 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -87,7 +87,16 @@ export async function run(): Promise { runner.addFile(join(__dirname, 'console/consoleAPITest')); runner.addFile(join(__dirname, 'extension/nodeConfigurationProvidersTests')); - for (const file of glob.sync('**/*.test.js', { cwd: __dirname })) { + const options = { cwd: __dirname }; + const files = glob.sync('**/*.test.js', options); + + // Only run tests on supported platforms + // https://nodejs.org/api/process.html#process_process_platform + if (process.platform === 'win32') { + files.push(...glob.sync('**/*.test.win.js', options)); + } + + for (const file of files) { runner.addFile(join(__dirname, file)); } } diff --git a/src/test/webview/webview-breakpoints-launched-script.txt b/src/test/webview/webview-breakpoints-launched-script.txt new file mode 100644 index 00000000..7e68970d --- /dev/null +++ b/src/test/webview/webview-breakpoints-launched-script.txt @@ -0,0 +1,16 @@ +{ + allThreadsStopped : false + description : Paused on debugger statement + reason : pause + threadId : +} + @ ${workspaceFolder}/web/script.js:10:1 +{ + allThreadsStopped : false + description : Paused on breakpoint + reason : breakpoint + threadId : +} +bar @ ${workspaceFolder}/web/script.js:6:3 +foo @ ${workspaceFolder}/web/script.js:2:3 + @ ${workspaceFolder}/web/script.js:11:1 diff --git a/src/test/webview/webview.breakpoints.test.win.ts b/src/test/webview/webview.breakpoints.test.win.ts new file mode 100644 index 00000000..30367bcc --- /dev/null +++ b/src/test/webview/webview.breakpoints.test.win.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import { ITestHandle } from '../test'; +import Dap from '../../dap/api'; +import { itIntegrates } from '../testIntegrationUtils'; +import { IChromeLaunchConfiguration } from '../../configuration'; +import { DebugType } from '../../common/contributionUtils'; + +describe('webview breakpoints', () => { + async function waitForPause(p: ITestHandle, cb?: (threadId: string) => Promise) { + const { threadId } = p.log(await p.dap.once('stopped')); + await p.logger.logStackTrace(threadId); + if (cb) await cb(threadId); + return p.dap.continue({ threadId }); + } + + itIntegrates('launched script', async ({ r }) => { + // Breakpoint in separate script set after launch. + const p = await r.launchUrl('script.html', ({ + type: DebugType.Edge, + runtimeExecutable: r.workspacePath('webview/win/WebView2Sample.exe'), + useWebView: true, + // WebView2Sample.exe launches about:blank + urlFilter: 'about:blank', + // TODO: Test.launchUrl should support AnyLaunchConfiguration + } as unknown) as IChromeLaunchConfiguration); + p.load(); + await waitForPause(p, async () => { + const source: Dap.Source = { + path: p.workspacePath('web/script.js'), + }; + await p.dap.setBreakpoints({ source, breakpoints: [{ line: 6 }] }); + }); + await waitForPause(p); + p.assertLog(); + }); +}); diff --git a/testWorkspace/webview/win/.gitignore b/testWorkspace/webview/win/.gitignore new file mode 100644 index 00000000..cd325741 --- /dev/null +++ b/testWorkspace/webview/win/.gitignore @@ -0,0 +1,2 @@ +# User directories created by WebView application +WebView2Sample.exe.WebView2*/ diff --git a/testWorkspace/webview/win/README.md b/testWorkspace/webview/win/README.md new file mode 100644 index 00000000..b8130524 --- /dev/null +++ b/testWorkspace/webview/win/README.md @@ -0,0 +1,2 @@ +The WebView2Sample.exe is a Release x64 build. +The source code can be found in `demos\webview`. diff --git a/testWorkspace/webview/win/WebView2Loader.dll b/testWorkspace/webview/win/WebView2Loader.dll new file mode 100644 index 00000000..1d68afbd Binary files /dev/null and b/testWorkspace/webview/win/WebView2Loader.dll differ diff --git a/testWorkspace/webview/win/WebView2Sample.exe b/testWorkspace/webview/win/WebView2Sample.exe new file mode 100644 index 00000000..0c12d6d7 Binary files /dev/null and b/testWorkspace/webview/win/WebView2Sample.exe differ diff --git a/testWorkspace/webview/win/WebView2Sample.pdb b/testWorkspace/webview/win/WebView2Sample.pdb new file mode 100644 index 00000000..144e803d Binary files /dev/null and b/testWorkspace/webview/win/WebView2Sample.pdb differ