Clean up int-chrome tests, port some (#306)
* Add hit condition tests * Add tests for conditional breakpoints * Delete unneeded int-chrome tests * Add test that runs create-react-app * Clean up testdata/ and remaining int-chrome references * Fix env var spelling * Fix "invalid condition" test * Categorize tests correctly * Disable framework tests for Windows * Make React test a substring test * Add new devops pipeline config
This commit is contained in:
Родитель
9991f3060a
Коммит
c1bb81457b
|
@ -1,5 +1,6 @@
|
|||
parameters:
|
||||
runTests: true
|
||||
runFrameworkTests: false
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
|
@ -36,6 +37,18 @@ steps:
|
|||
env:
|
||||
DISPLAY: ':99.0'
|
||||
|
||||
- task: Npm@1
|
||||
displayName: npm test (framework tests)
|
||||
inputs:
|
||||
command: custom
|
||||
verbose: false
|
||||
customCommand: test
|
||||
timeoutInMinutes: 10
|
||||
condition: eq(${{ parameters.runFrameworkTests }}, true)
|
||||
env:
|
||||
FRAMEWORK_TESTS: 1
|
||||
DISPLAY: ':99.0'
|
||||
|
||||
- task: Gulp@0
|
||||
displayName: gulp lint
|
||||
inputs:
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# Run on a schedule
|
||||
trigger: none
|
||||
pr: none
|
||||
|
||||
jobs:
|
||||
- job: macOS
|
||||
pool:
|
||||
vmImage: 'macOS-10.13'
|
||||
steps:
|
||||
- template: common-validation.yml
|
||||
parameters:
|
||||
runFrameworkTests: true
|
||||
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- template: common-validation.yml
|
||||
parameters:
|
||||
runFrameworkTests: true
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
steps:
|
||||
- template: common-validation.yml
|
|
@ -1,10 +1,10 @@
|
|||
module.exports = {
|
||||
ignorePatterns: [
|
||||
'**/*.d.ts',
|
||||
'src/int-chrome/**/*.ts',
|
||||
'src/test/**/*.ts',
|
||||
'demos/**/*',
|
||||
'**/*.js',
|
||||
'testWorkspace/**'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: ['plugin:@typescript-eslint/recommended'],
|
||||
|
|
|
@ -85,20 +85,6 @@
|
|||
],
|
||||
"outFiles": ["${workspaceFolder}/out/src/test/**/*.js"],
|
||||
// "preLaunchTask": "npm: watch"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Run int tests",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": ["--timeout", "10000", "-u", "tdd", "--colors", "--reporter", "out/src/int-chrome/testSupport/loggingReporter.js", "./out/src/int-chrome/**/*.test.js",
|
||||
// "--grep", "Variables scopes"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/out/src/**/*.js"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"env": {
|
||||
"DISPLAY": ":1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Reset Results",
|
||||
|
|
|
@ -45,8 +45,7 @@
|
|||
"publish": "gulp publish",
|
||||
"test": "gulp && npm-run-all --parallel test:golden test:lint",
|
||||
"test:golden": "node ./out/src/test/runTest.js",
|
||||
"test:lint": "gulp lint",
|
||||
"intTest": "mocha --exit --timeout 20000 -s 3500 -u tdd --colors --reporter out/src/int-chrome/testSupport/loggingReporter.js \"./out/src/int-chrome/**/*.test.js\""
|
||||
"test:lint": "gulp lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@c4312/chromehash": "^0.2.0",
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { expect } from 'chai';
|
||||
import { createServer } from 'http-server';
|
||||
import * as path from 'path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as testSetup from './testSetup';
|
||||
import { ExtendedDebugClient } from './testSupport/debugClient';
|
||||
import { IChromeTestAttachConfiguration, killAllChrome, retryAsync } from './testUtils';
|
||||
import { HttpOrHttpsServer } from './types/server';
|
||||
import { getDebugAdapterLogFilePath } from './utils/logging';
|
||||
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
|
||||
suite('Chrome Debug Adapter etc', () => {
|
||||
let dc: ExtendedDebugClient;
|
||||
let server: HttpOrHttpsServer | null;
|
||||
|
||||
setup(function() {
|
||||
return testSetup.setup(this).then(_dc => (dc = _dc));
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
return testSetup.teardown();
|
||||
});
|
||||
|
||||
suite('basic', () => {
|
||||
test('unknown request should produce error', done => {
|
||||
dc.send('illegal_request')
|
||||
.then(() => {
|
||||
done(new Error('does not report error on unknown request'));
|
||||
})
|
||||
.catch(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('initialize', () => {
|
||||
test('should return supported features', () => {
|
||||
return dc.initializeRequest().then(response => {
|
||||
assert.notEqual(response.body, undefined);
|
||||
assert.equal(response.body!.supportsConfigurationDoneRequest, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('launch', () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'intervalDebugger');
|
||||
setup(() => {
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server!.listen(7890);
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
if (server) {
|
||||
server.close(err =>
|
||||
console.log('Error closing server in teardown: ' + (err && err.message)),
|
||||
);
|
||||
server = null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* On MacOS it fails because: stopped location: path mismatch:
|
||||
* + expected: /users/vsts/agent/2.150.0/work/1/s/testdata/intervaldebugger/out/app.js
|
||||
* - actual: users/vsts/agent/2.150.0/work/1/s/testdata/intervaldebugger/out/app.js
|
||||
*/
|
||||
(testSetup.isWindows ? test : test.skip)(
|
||||
'should stop on debugger statement in file:///, sourcemaps disabled',
|
||||
() => {
|
||||
const launchFile = path.join(testProjectRoot, 'index.html');
|
||||
const breakFile = path.join(testProjectRoot, 'out/app.js');
|
||||
const DEBUGGER_LINE = 2;
|
||||
|
||||
return Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.launch({ file: launchFile, sourceMaps: false }),
|
||||
dc.assertStoppedLocation('debugger_statement', { path: breakFile, line: DEBUGGER_LINE }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test('should stop on debugger statement in http://localhost', () => {
|
||||
const breakFile = path.join(testProjectRoot, 'src/app.ts');
|
||||
const DEBUGGER_LINE = 2;
|
||||
|
||||
return Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.launch({ url: 'http://localhost:7890', webRoot: testProjectRoot }),
|
||||
dc.assertStoppedLocation('pause', { path: breakFile, line: DEBUGGER_LINE }),
|
||||
]);
|
||||
});
|
||||
|
||||
const testTitle =
|
||||
'Should attach to existing instance of chrome and break on debugger statement';
|
||||
test(testTitle, async () => {
|
||||
const fullTestTitle = `Chrome Debug Adapter etc launch ${testTitle}`;
|
||||
const breakFile = path.join(testProjectRoot, 'src/app.ts');
|
||||
const DEBUGGER_LINE = 2;
|
||||
const remoteDebuggingPort = 7777;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: ['http://localhost:7890', `--remote-debugging-port=${remoteDebuggingPort}`],
|
||||
});
|
||||
try {
|
||||
await Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.initializeRequest().then(_ => {
|
||||
return dc.attachRequest(<IChromeTestAttachConfiguration>{
|
||||
url: 'http://localhost:7890',
|
||||
port: remoteDebuggingPort,
|
||||
webRoot: testProjectRoot,
|
||||
logFilePath: getDebugAdapterLogFilePath(fullTestTitle),
|
||||
logTimestamps: true,
|
||||
});
|
||||
}),
|
||||
dc.assertStoppedLocation('debugger_statement', { path: breakFile, line: DEBUGGER_LINE }),
|
||||
]);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
});
|
||||
|
||||
test.skip('Should hit breakpoint even if webRoot has unexpected case all lowercase for VisualStudio', async () => {
|
||||
const breakFile = path.join(testProjectRoot, 'src/app.ts');
|
||||
const DEBUGGER_LINE = 2;
|
||||
|
||||
await dc.initializeRequest({
|
||||
adapterID: 'chrome',
|
||||
clientID: 'visualstudio',
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true,
|
||||
pathFormat: 'path',
|
||||
});
|
||||
|
||||
await dc.launchRequest({
|
||||
url: 'http://localhost:7890',
|
||||
webRoot: testProjectRoot.toLowerCase(),
|
||||
runtimeExecutable: puppeteer.executablePath(),
|
||||
} as any);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: breakFile },
|
||||
breakpoints: [{ line: DEBUGGER_LINE }],
|
||||
});
|
||||
await dc.configurationDoneRequest();
|
||||
await dc.assertStoppedLocation('debugger_statement', {
|
||||
path: breakFile,
|
||||
line: DEBUGGER_LINE,
|
||||
});
|
||||
});
|
||||
|
||||
test.skip('Should hit breakpoint even if webRoot has unexpected case all uppercase for VisualStudio', async () => {
|
||||
const breakFile = path.join(testProjectRoot, 'src/app.ts');
|
||||
const DEBUGGER_LINE = 2;
|
||||
|
||||
await dc.initializeRequest({
|
||||
adapterID: 'chrome',
|
||||
clientID: 'visualstudio',
|
||||
linesStartAt1: true,
|
||||
columnsStartAt1: true,
|
||||
pathFormat: 'path',
|
||||
});
|
||||
await dc.launchRequest({
|
||||
url: 'http://localhost:7890',
|
||||
webRoot: testProjectRoot.toUpperCase(),
|
||||
runtimeExecutable: puppeteer.executablePath(),
|
||||
} as any);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: breakFile },
|
||||
breakpoints: [{ line: DEBUGGER_LINE }],
|
||||
});
|
||||
await dc.configurationDoneRequest();
|
||||
await dc.assertStoppedLocation('debugger_statement', {
|
||||
path: breakFile,
|
||||
line: DEBUGGER_LINE,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This test is baselining behvaior from V1 around what happens when the adapter tries to launch when
|
||||
* there is another running instance of chrome with --remote-debugging-port set to the same port the adapter is trying to use.
|
||||
* We expect the debug adapter to throw an exception saying that the connection attempt timed out after N milliseconds.
|
||||
* TODO: We don't think is is ideal behavior for the adapter, and want to change it fairly quickly after V2 is ready to launch.
|
||||
* right now this test exists only to verify that we match the behavior of V1
|
||||
*/
|
||||
test('Should throw error when launching if chrome debug port is in use', async () => {
|
||||
// browser already launched to the default port, and navigated away from about:blank
|
||||
const remoteDebuggingPort = 9222;
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: ['http://localhost:7890', `--remote-debugging-port=${remoteDebuggingPort}`],
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.launch({
|
||||
url: 'http://localhost:7890',
|
||||
timeout: 2000,
|
||||
webRoot: testProjectRoot,
|
||||
port: remoteDebuggingPort,
|
||||
}),
|
||||
]);
|
||||
assert.fail("Expected launch to throw a timeout exception, but it didn't.");
|
||||
} catch (err) {
|
||||
expect(err.message).to.satisfy((x: string) =>
|
||||
x.startsWith('Cannot connect to runtime process, timeout after 2000 ms'),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// force kill chrome here, as it will be left open by the debug adapter (same behavior as v1)
|
||||
killAllChrome();
|
||||
});
|
||||
|
||||
test('Should launch successfully on port 0', async () => {
|
||||
// browser already launched to the default port, and navigated away from about:blank
|
||||
const remoteDebuggingPort = 0;
|
||||
await Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.launch({
|
||||
url: 'http://localhost:7890',
|
||||
timeout: 5000,
|
||||
webRoot: testProjectRoot,
|
||||
port: remoteDebuggingPort,
|
||||
}),
|
||||
]);
|
||||
|
||||
// wait for url to === http://localhost:7890 (launch response can come back before the navigation completes)
|
||||
return waitForUrl(dc, 'http://localhost:7890/');
|
||||
});
|
||||
|
||||
test('Should launch successfully on port 0, even when a browser instance is already running', async () => {
|
||||
// browser already launched to the default port, and navigated away from about:blank
|
||||
const remoteDebuggingPort = 0;
|
||||
const dataDir = path.join(__dirname, 'testDataDir');
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
args: [
|
||||
'https://bing.com',
|
||||
`--user-data-dir=${dataDir}`,
|
||||
`--remote-debugging-port=${remoteDebuggingPort}`,
|
||||
],
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
dc.configurationSequence(),
|
||||
dc.launch({
|
||||
url: 'http://localhost:7890',
|
||||
timeout: 5000,
|
||||
webRoot: testProjectRoot,
|
||||
port: remoteDebuggingPort,
|
||||
userDataDir: dataDir,
|
||||
}),
|
||||
]);
|
||||
|
||||
await waitForUrl(dc, 'http://localhost:7890/');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForUrl(dc: ExtendedDebugClient, url: string): Promise<string> {
|
||||
const timeoutMs = 5000;
|
||||
const intervalDelayMs = 50;
|
||||
|
||||
return await retryAsync(
|
||||
async () => {
|
||||
const response = await dc.evaluateRequest({
|
||||
context: 'repl',
|
||||
expression: 'window.location.href',
|
||||
});
|
||||
|
||||
expect(response.body.result).to.equal(`"${url}"`);
|
||||
return url;
|
||||
},
|
||||
timeoutMs,
|
||||
intervalDelayMs,
|
||||
).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
|
@ -1,323 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { createServer } from 'http-server';
|
||||
|
||||
import * as testSetup from './testSetup';
|
||||
import { HttpOrHttpsServer } from './types/server';
|
||||
import { ExtendedDebugClient } from './testSupport/debugClient';
|
||||
import { chromeLaunchConfigDefaults } from '../configuration';
|
||||
|
||||
suite('BreakOnLoad', () => {
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
|
||||
let dc: ExtendedDebugClient;
|
||||
setup(function() {
|
||||
return testSetup
|
||||
.setup(this, {
|
||||
sourceMapPathOverrides: chromeLaunchConfigDefaults.sourceMapPathOverrides,
|
||||
})
|
||||
.then(_dc => (dc = _dc));
|
||||
});
|
||||
|
||||
let server: HttpOrHttpsServer | null;
|
||||
teardown(() => {
|
||||
if (server) {
|
||||
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
|
||||
server = null;
|
||||
}
|
||||
|
||||
return testSetup.teardown();
|
||||
});
|
||||
|
||||
// this function is to help when launching and setting a breakpoint
|
||||
// currently, the chrome debug adapter, when launching in instrument mode and setting a breakpoint at (1, 1)
|
||||
// the breakpoint is not yet 'hit' so the reason is given as 'debugger_statement'
|
||||
// https://github.com/Microsoft/vscode-chrome-debug-core/blob/90797bc4a3599b0a7c0f197efe10ef7fab8442fd/src/chrome/chromeDebugAdapter.ts#L692
|
||||
// so we don't want to use hitBreakpointUnverified function because it specifically checks for 'breakpoint' as the reason
|
||||
function launchWithUrlAndSetBreakpoints(
|
||||
url: string,
|
||||
projectRoot: string,
|
||||
scriptPath: string,
|
||||
lineNum: number,
|
||||
colNum: number,
|
||||
): Promise<any> {
|
||||
return Promise.all([
|
||||
dc.launch({ url: url, webRoot: projectRoot }),
|
||||
dc
|
||||
.waitForEvent('initialized')
|
||||
.then(_event => {
|
||||
return dc.setBreakpointsRequest({
|
||||
lines: [lineNum],
|
||||
breakpoints: [{ line: lineNum, column: colNum }],
|
||||
source: { path: scriptPath },
|
||||
});
|
||||
})
|
||||
.then(_response => {
|
||||
return dc.configurationDoneRequest();
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
suite('TypeScript Project with SourceMaps', () => {
|
||||
test('Hits a single breakpoint in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 3;
|
||||
const bpCol = 11;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
|
||||
test('Hits multiple breakpoints in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bp1Line = 3;
|
||||
const bp1Col = 11;
|
||||
const bp2Line = 6;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bp1Line, column: bp1Col },
|
||||
);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: scriptPath },
|
||||
breakpoints: [{ line: bp2Line }],
|
||||
});
|
||||
await dc.continueTo('breakpoint', { line: bp2Line });
|
||||
});
|
||||
|
||||
test('Hits a breakpoint at (1,1) in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 1;
|
||||
|
||||
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
|
||||
await dc.assertStoppedLocation('breakpoint', {
|
||||
path: scriptPath,
|
||||
line: bpLine,
|
||||
column: bpCol,
|
||||
});
|
||||
});
|
||||
|
||||
test('Hits a breakpoint in the first line in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_sourceMaps');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 34;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Simple JavaScript Project', () => {
|
||||
test('Hits a single breakpoint in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 3;
|
||||
const bpCol = 12;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
|
||||
test('Hits multiple breakpoints in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bp1Line = 3;
|
||||
const bp1Col = 12;
|
||||
const bp2Line = 6;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bp1Line, column: bp1Col },
|
||||
);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: scriptPath },
|
||||
breakpoints: [{ line: bp2Line }],
|
||||
});
|
||||
await dc.continueTo('breakpoint', { line: bp2Line });
|
||||
});
|
||||
|
||||
test('Hits a breakpoint at (1,1) in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 1;
|
||||
|
||||
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
|
||||
await dc.assertStoppedLocation('breakpoint', {
|
||||
path: scriptPath,
|
||||
line: bpLine,
|
||||
column: bpCol,
|
||||
});
|
||||
});
|
||||
|
||||
test('Hits a breakpoint in the first line in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 35;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
|
||||
test('Hits breakpoints on the first line of two scripts', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_javaScript');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
const script2Path = path.join(testProjectRoot, 'src/script2.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 1;
|
||||
|
||||
await launchWithUrlAndSetBreakpoints(url, testProjectRoot, scriptPath, bpLine, bpCol);
|
||||
await dc.assertStoppedLocation('breakpoint', {
|
||||
path: scriptPath,
|
||||
line: bpLine,
|
||||
column: bpCol,
|
||||
});
|
||||
await dc.setBreakpointsRequest({
|
||||
lines: [bpLine],
|
||||
breakpoints: [{ line: bpLine, column: bpCol }],
|
||||
source: { path: script2Path },
|
||||
});
|
||||
await dc.continueRequest();
|
||||
await dc.assertStoppedLocation('breakpoint', {
|
||||
path: script2Path,
|
||||
line: bpLine,
|
||||
column: bpCol,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Instrument Webpack Project', () => {
|
||||
test('Hits a single breakpoint in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/dist/index.html';
|
||||
|
||||
const bpLine = 3;
|
||||
const bpCol = 1;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
|
||||
test('Hits multiple breakpoints in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/dist/index.html';
|
||||
|
||||
// For some reason, column numbers > don't work perfectly with webpack
|
||||
const bp1Line = 3;
|
||||
const bp1Col = 1;
|
||||
const bp2Line = 5;
|
||||
const bp2Col = 1;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bp1Line, column: bp1Col },
|
||||
);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: scriptPath },
|
||||
breakpoints: [{ line: bp2Line, column: bp2Col }],
|
||||
});
|
||||
await dc.continueTo('breakpoint', { line: bp2Line, column: bp2Col });
|
||||
});
|
||||
|
||||
test('Hits a breakpoint at (1,1) in a file on load', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'breakOnLoad_webpack');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/dist/index.html';
|
||||
|
||||
const bpLine = 1;
|
||||
const bpCol = 1;
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,162 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { createServer } from 'http-server';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as testSetup from './testSetup';
|
||||
import { HttpOrHttpsServer } from './types/server';
|
||||
import { ExtendedDebugClient } from './testSupport/debugClient';
|
||||
|
||||
suite('Breakpoints', () => {
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
|
||||
let dc: ExtendedDebugClient;
|
||||
setup(function() {
|
||||
return testSetup.setup(this).then(_dc => (dc = _dc));
|
||||
});
|
||||
|
||||
let server: HttpOrHttpsServer | null;
|
||||
teardown(async () => {
|
||||
if (server) {
|
||||
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
|
||||
server = null;
|
||||
}
|
||||
|
||||
await testSetup.teardown();
|
||||
});
|
||||
|
||||
suite('getPossibleBreakpoints', () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'columns');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
setup(async () => {
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: 4, column: 16 },
|
||||
);
|
||||
});
|
||||
|
||||
test('gets breakpoints on a single line', async () => {
|
||||
const result = await dc.breakpointLocations({
|
||||
source: { path: scriptPath },
|
||||
line: 4,
|
||||
});
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
breakpoints: [
|
||||
{ line: 4, column: 9 },
|
||||
{ line: 4, column: 16 },
|
||||
{ line: 4, column: 24 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('gets breakpoints across multiple lines', async () => {
|
||||
const result = await dc.breakpointLocations({
|
||||
source: { path: scriptPath },
|
||||
line: 3,
|
||||
column: 2,
|
||||
endLine: 4,
|
||||
endColumn: 20,
|
||||
});
|
||||
|
||||
expect(result).to.deep.equal({
|
||||
breakpoints: [
|
||||
{ line: 3, column: 18 },
|
||||
{ line: 3, column: 21 },
|
||||
{ line: 3, column: 28 },
|
||||
{ line: 4, column: 9 },
|
||||
{ line: 4, column: 16 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('no-ops if getting breakpoints outside range', async () => {
|
||||
const result = await dc.breakpointLocations({
|
||||
source: { path: scriptPath },
|
||||
line: 100,
|
||||
});
|
||||
|
||||
expect(result.breakpoints).to.be.empty;
|
||||
});
|
||||
|
||||
test('no-ops if getting a non-existent file', async () => {
|
||||
const result = await dc.breakpointLocations({
|
||||
source: { path: 'potato.js' },
|
||||
line: 100,
|
||||
});
|
||||
|
||||
expect(result.breakpoints).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
suite('Column BPs', () => {
|
||||
test('Column BP is hit on correct column', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'columns');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 4;
|
||||
|
||||
const bpCol = 16;
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol },
|
||||
);
|
||||
});
|
||||
|
||||
test('Multiple column BPs are hit on correct columns', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'columns');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 4;
|
||||
const bpCol1 = 16;
|
||||
const bpCol2 = 24;
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol1 },
|
||||
);
|
||||
await dc.setBreakpointsRequest({
|
||||
source: { path: scriptPath },
|
||||
breakpoints: [{ line: bpLine, column: bpCol2 }],
|
||||
});
|
||||
await dc.continueTo('breakpoint', { line: bpLine, column: bpCol2 });
|
||||
});
|
||||
|
||||
test.skip('BP col is adjusted to correct col', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'columns');
|
||||
const scriptPath = path.join(testProjectRoot, 'src/script.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
const bpLine = 4;
|
||||
const bpCol1 = 19;
|
||||
const correctBpCol1 = 16;
|
||||
const expectedLocation = { path: scriptPath, line: bpLine, column: correctBpCol1 };
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, webRoot: testProjectRoot },
|
||||
{ path: scriptPath, line: bpLine, column: bpCol1 },
|
||||
expectedLocation,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
export function asyncMap<T, U>(
|
||||
array: ReadonlyArray<T>,
|
||||
callbackfn: (value: T, index: number, array: ReadonlyArray<T>) => Promise<U> | U,
|
||||
thisArg?: any,
|
||||
): Promise<U[]> {
|
||||
return Promise.all(array.map(callbackfn, thisArg));
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ValidatedMap } from './validatedMap';
|
||||
import { printMap } from './printing';
|
||||
import { breakWhileDebugging } from '../../validation';
|
||||
|
||||
/** A map where we can efficiently get the key from the value or the value from the key */
|
||||
export class BidirectionalMap<Left, Right> {
|
||||
private readonly _leftToRight = new ValidatedMap<Left, Right>();
|
||||
private readonly _rightToLeft = new ValidatedMap<Right, Left>();
|
||||
|
||||
constructor(initialContents?: Iterable<[Left, Right]> | ReadonlyArray<[Left, Right]>) {
|
||||
this._leftToRight = initialContents
|
||||
? new ValidatedMap<Left, Right>(initialContents)
|
||||
: new ValidatedMap<Left, Right>();
|
||||
const reversed = Array.from(this._leftToRight.entries()).map(e => <[Right, Left]>[e[1], e[0]]);
|
||||
this._rightToLeft = new ValidatedMap<Right, Left>(reversed);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._leftToRight.clear();
|
||||
this._rightToLeft.clear();
|
||||
}
|
||||
|
||||
public deleteByLeft(left: Left): boolean {
|
||||
const right = this._leftToRight.get(left);
|
||||
if (right !== undefined) {
|
||||
this.delete(left, right);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public deleteByRight(right: Right): boolean {
|
||||
const left = this._rightToLeft.get(right);
|
||||
if (left !== undefined) {
|
||||
this.delete(left, right);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private delete(left: Left, right: Right): void {
|
||||
assert.ok(
|
||||
this._leftToRight.delete(left),
|
||||
`Expected left (${left}) associated with right (${right}) to exist on the left to right internal map`,
|
||||
);
|
||||
assert.ok(
|
||||
this._rightToLeft.delete(right),
|
||||
`Expected right (${right}) associated with left (${left}) to exist on the right to left internal map`,
|
||||
);
|
||||
}
|
||||
|
||||
public forEach(
|
||||
callbackfn: (Right: Right, left: Left, map: Map<Left, Right>) => void,
|
||||
thisArg?: any,
|
||||
): void {
|
||||
return this._leftToRight.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
public getByLeft(left: Left): Right {
|
||||
return this._leftToRight.get(left);
|
||||
}
|
||||
|
||||
public getByRight(right: Right): Left {
|
||||
return this._rightToLeft.get(right);
|
||||
}
|
||||
|
||||
public tryGettingByLeft(left: Left): Right | undefined {
|
||||
return this._leftToRight.tryGetting(left);
|
||||
}
|
||||
|
||||
public tryGettingByRight(right: Right): Left | undefined {
|
||||
return this._rightToLeft.tryGetting(right);
|
||||
}
|
||||
|
||||
public hasLeft(left: Left): boolean {
|
||||
return this._leftToRight.has(left);
|
||||
}
|
||||
|
||||
public hasRight(right: Right): boolean {
|
||||
return this._rightToLeft.has(right);
|
||||
}
|
||||
|
||||
public set(left: Left, right: Right): this {
|
||||
const existingRightForLeft = this._leftToRight.tryGetting(left);
|
||||
const existingLeftForRight = this._rightToLeft.tryGetting(right);
|
||||
|
||||
if (existingRightForLeft !== undefined) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Can't set the pair left (${left}) and right (${right}) because there is already a right element (${existingRightForLeft}) associated with the left element`,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingLeftForRight !== undefined) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Can't set the pair left (${left}) and right (${right}) because there is already a left element (${existingLeftForRight}) associated with the right element`,
|
||||
);
|
||||
}
|
||||
|
||||
this._leftToRight.set(left, right);
|
||||
this._rightToLeft.set(right, left);
|
||||
return this;
|
||||
}
|
||||
|
||||
public size(): number {
|
||||
return this._leftToRight.size;
|
||||
}
|
||||
|
||||
public lefts(): IterableIterator<Left> {
|
||||
return this._leftToRight.keys();
|
||||
}
|
||||
|
||||
public rights(): IterableIterator<Right> {
|
||||
return this._rightToLeft.keys();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return printMap('BidirectionalMap', this._leftToRight);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/** Methods to print the contents of a collection for logging and debugging purposes (This is not intended for the end-user to see) */
|
||||
export function printMap<K, V>(
|
||||
typeDescription: string,
|
||||
map: { entries(): IterableIterator<[K, V]> },
|
||||
): string {
|
||||
const elementsPrinted = Array.from(map.entries())
|
||||
.map(entry => `${entry[0]}: ${entry[1]}`)
|
||||
.join('; ');
|
||||
return `${typeDescription} { ${elementsPrinted} }`;
|
||||
}
|
||||
|
||||
export function printSet<T>(typeDescription: string, set: Set<T>): string {
|
||||
const elementsPrinted = printElements(Array.from(set), '; ');
|
||||
return `${typeDescription} { ${elementsPrinted} }`;
|
||||
}
|
||||
|
||||
export function printArray<T>(typeDescription: string, elements: T[]): string {
|
||||
const elementsPrinted = printElements(elements, ', ');
|
||||
return typeDescription ? `${typeDescription} [ ${elementsPrinted} ]` : `[ ${elementsPrinted} ]`;
|
||||
}
|
||||
|
||||
export function printIterable<T>(typeDescription: string, iterable: IterableIterator<T>): string {
|
||||
const elementsPrinted = printElements(Array.from(iterable), '; ');
|
||||
return `${typeDescription} { ${elementsPrinted} }`;
|
||||
}
|
||||
|
||||
function printElements<T>(elements: T[], separator = '; '): string {
|
||||
return elements.map(element => `${element}`).join(separator);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { ValidatedMultiMap } from './validatedMultiMap';
|
||||
|
||||
export function groupByKey<T, K>(
|
||||
elements: T[],
|
||||
obtainKey: (element: T) => K,
|
||||
): ValidatedMultiMap<K, T> {
|
||||
const grouped = ValidatedMultiMap.empty<K, T>();
|
||||
elements.forEach(element => grouped.add(obtainKey(element), element));
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export function determineOrderingOfStrings(left: string, right: string): number {
|
||||
if (left < right) {
|
||||
return -1;
|
||||
} else if (left > right) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function singleElementOfArray<T>(array: ReadonlyArray<T>): T {
|
||||
if (array.length === 1) {
|
||||
return array[0];
|
||||
} else {
|
||||
throw new Error(
|
||||
`Expected array ${array} to have exactly a single element yet it had ${array.length}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { printMap } from './printing';
|
||||
import { breakWhileDebugging } from '../../validation';
|
||||
|
||||
export type ValueComparerFunction<V> = (left: V, right: V) => boolean;
|
||||
|
||||
export interface IValidatedMap<K, V> extends Map<K, V> {
|
||||
get(key: K): V;
|
||||
tryGetting(key: K): V | undefined;
|
||||
getOr(key: K, elementDoesntExistAction: () => V): V;
|
||||
getOrAdd(key: K, obtainValueToAdd: () => V): V;
|
||||
setAndReplaceIfExist(key: K, value: V): this;
|
||||
setAndIgnoreDuplicates(key: K, value: V, comparer?: ValueComparerFunction<V>): this;
|
||||
}
|
||||
|
||||
/** A map that throws exceptions instead of returning error codes. */
|
||||
export class ValidatedMap<K, V> implements IValidatedMap<K, V> {
|
||||
private readonly _wrappedMap: Map<K, V>;
|
||||
|
||||
constructor(initialContents?: Map<K, V>);
|
||||
constructor(iterable: Iterable<[K, V]>);
|
||||
constructor(array: ReadonlyArray<[K, V]>);
|
||||
constructor(initialContents?: Map<K, V> | Iterable<[K, V]> | ReadonlyArray<[K, V]>) {
|
||||
if (initialContents !== undefined) {
|
||||
this._wrappedMap =
|
||||
initialContents instanceof Map
|
||||
? new Map<K, V>(initialContents.entries())
|
||||
: new Map<K, V>(initialContents);
|
||||
} else {
|
||||
this._wrappedMap = new Map<K, V>();
|
||||
}
|
||||
}
|
||||
|
||||
public static with<K, V>(key: K, value: V): ValidatedMap<K, V> {
|
||||
return new ValidatedMap<K, V>([[key, value]]);
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this._wrappedMap.size;
|
||||
}
|
||||
|
||||
public get [Symbol.toStringTag](): 'Map' {
|
||||
return 'ValidatedMap' as 'Map';
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._wrappedMap.clear();
|
||||
}
|
||||
|
||||
public delete(key: K): boolean {
|
||||
if (!this._wrappedMap.delete(key)) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Couldn't delete element with key ${key} because it wasn't present in the map`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
|
||||
this._wrappedMap.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
public get(key: K): V {
|
||||
const value = this._wrappedMap.get(key);
|
||||
if (value === undefined) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Couldn't get the element with key '${key}' because it wasn't present in this map <${this}>`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public getOr(key: K, elementDoesntExistAction: () => V): V {
|
||||
const existingValue = this.tryGetting(key);
|
||||
if (existingValue !== undefined) {
|
||||
return existingValue;
|
||||
} else {
|
||||
return elementDoesntExistAction();
|
||||
}
|
||||
}
|
||||
|
||||
public getOrAdd(key: K, obtainValueToAdd: () => V): V {
|
||||
return this.getOr(key, () => {
|
||||
const newValue = obtainValueToAdd();
|
||||
this.set(key, newValue);
|
||||
return newValue;
|
||||
});
|
||||
}
|
||||
|
||||
public has(key: K): boolean {
|
||||
return this._wrappedMap.has(key);
|
||||
}
|
||||
|
||||
public set(key: K, value: V): this {
|
||||
if (this.has(key)) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(`Cannot set key ${key} because it already exists`);
|
||||
}
|
||||
|
||||
return this.setAndReplaceIfExist(key, value);
|
||||
}
|
||||
|
||||
public setAndReplaceIfExist(key: K, value: V): this {
|
||||
this._wrappedMap.set(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public setAndIgnoreDuplicates(
|
||||
key: K,
|
||||
value: V,
|
||||
comparer: ValueComparerFunction<V> = (left, right) => left === right,
|
||||
) {
|
||||
const existingValueOrUndefined = this.tryGetting(key);
|
||||
if (existingValueOrUndefined !== undefined && !comparer(existingValueOrUndefined, value)) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Cannot set key ${key} for value ${value} because it already exists and it's associated to a different value: ${existingValueOrUndefined}`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.setAndReplaceIfExist(key, value);
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
return this._wrappedMap.entries();
|
||||
}
|
||||
|
||||
public entries(): IterableIterator<[K, V]> {
|
||||
return this._wrappedMap.entries();
|
||||
}
|
||||
|
||||
public keys(): IterableIterator<K> {
|
||||
return this._wrappedMap.keys();
|
||||
}
|
||||
|
||||
public values(): IterableIterator<V> {
|
||||
return this._wrappedMap.values();
|
||||
}
|
||||
|
||||
// TODO: Remove the use of undefined
|
||||
public tryGetting(key: K): V | undefined {
|
||||
return this._wrappedMap.get(key) || undefined;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return printMap('ValidatedMap', this);
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { ValidatedMap, IValidatedMap } from './validatedMap';
|
||||
import { printMap } from './printing';
|
||||
import { ValidatedSet, IValidatedSet } from './validatedSet';
|
||||
|
||||
/** A multi map that throws exceptions instead of returning error codes. */
|
||||
export class ValidatedMultiMap<K, V> {
|
||||
public get keysSize(): number {
|
||||
return this._wrappedMap.size;
|
||||
}
|
||||
|
||||
public get [Symbol.toStringTag](): 'Map' {
|
||||
return 'ValidatedMultiMap' as 'Map';
|
||||
}
|
||||
|
||||
private constructor(private readonly _wrappedMap: IValidatedMap<K, IValidatedSet<V>>) {}
|
||||
|
||||
public static empty<K, V>(): ValidatedMultiMap<K, V> {
|
||||
return this.usingCustomMap(new ValidatedMap<K, IValidatedSet<V>>());
|
||||
}
|
||||
|
||||
public static withContents<K, V>(
|
||||
initialContents: Map<K, Set<V>> | Iterable<[K, Set<V>]> | ReadonlyArray<[K, Set<V>]>,
|
||||
): ValidatedMultiMap<K, V> {
|
||||
const elements = Array.from(initialContents).map(
|
||||
element => <[K, IValidatedSet<V>]>[element[0], new ValidatedSet(element[1])],
|
||||
);
|
||||
return this.usingCustomMap(new ValidatedMap<K, IValidatedSet<V>>(elements));
|
||||
}
|
||||
|
||||
public static usingCustomMap<K, V>(
|
||||
wrappedMap: IValidatedMap<K, IValidatedSet<V>>,
|
||||
): ValidatedMultiMap<K, V> {
|
||||
return new ValidatedMultiMap(wrappedMap);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._wrappedMap.clear();
|
||||
}
|
||||
|
||||
public delete(key: K): boolean {
|
||||
return this._wrappedMap.delete(key);
|
||||
}
|
||||
|
||||
public forEach(
|
||||
callbackfn: (value: Set<V>, key: K, map: Map<K, Set<V>>) => void,
|
||||
thisArg?: any,
|
||||
): void {
|
||||
this._wrappedMap.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
public get(key: K): Set<V> {
|
||||
return this._wrappedMap.get(key);
|
||||
}
|
||||
|
||||
public getOr(key: K, elementDoesntExistAction: () => Set<V>): Set<V> {
|
||||
return this._wrappedMap.getOr(key, () => new ValidatedSet(elementDoesntExistAction()));
|
||||
}
|
||||
|
||||
public has(key: K): boolean {
|
||||
return this._wrappedMap.has(key);
|
||||
}
|
||||
|
||||
public addKeyIfNotExistant(key: K): this {
|
||||
const existingValues = this._wrappedMap.tryGetting(key);
|
||||
if (existingValues === undefined) {
|
||||
this._wrappedMap.set(key, new ValidatedSet());
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public add(key: K, value: V): this {
|
||||
const existingValues = this._wrappedMap.tryGetting(key);
|
||||
if (existingValues !== undefined) {
|
||||
existingValues.add(value);
|
||||
} else {
|
||||
this._wrappedMap.set(key, new ValidatedSet([value]));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public addAndIgnoreDuplicates(key: K, value: V): this {
|
||||
const existingValues = this._wrappedMap.tryGetting(key);
|
||||
if (existingValues !== undefined) {
|
||||
existingValues.addOrReplaceIfExists(value);
|
||||
} else {
|
||||
this._wrappedMap.set(key, new ValidatedSet([value]));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public removeValueAndIfLastRemoveKey(key: K, value: V): this {
|
||||
const remainingValues = this.removeValue(key, value);
|
||||
|
||||
if (remainingValues.size === 0) {
|
||||
this._wrappedMap.delete(key);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public removeValue(key: K, value: V): Set<V> {
|
||||
const existingValues = this._wrappedMap.get(key);
|
||||
if (!existingValues.delete(value)) {
|
||||
throw new Error(
|
||||
`Failed to delete the value ${value} under key ${key} because it wasn't present`,
|
||||
);
|
||||
}
|
||||
|
||||
return existingValues;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<[K, Set<V>]> {
|
||||
return this._wrappedMap.entries();
|
||||
}
|
||||
|
||||
public entries(): IterableIterator<[K, Set<V>]> {
|
||||
return this._wrappedMap.entries();
|
||||
}
|
||||
|
||||
public keys(): IterableIterator<K> {
|
||||
return this._wrappedMap.keys();
|
||||
}
|
||||
|
||||
public values(): IterableIterator<Set<V>> {
|
||||
return this._wrappedMap.values();
|
||||
}
|
||||
|
||||
public tryGetting(key: K): Set<V> | undefined {
|
||||
return this._wrappedMap.tryGetting(key);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return printMap('ValidatedMultiMap', this);
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { printSet } from './printing';
|
||||
import { breakWhileDebugging } from '../../validation';
|
||||
|
||||
export interface IValidatedSet<K> extends Set<K> {
|
||||
addOrReplaceIfExists(key: K): this;
|
||||
deleteIfExists(key: K): boolean;
|
||||
toArray(): K[];
|
||||
}
|
||||
|
||||
/** A set that throws exceptions instead of returning error codes. */
|
||||
export class ValidatedSet<K> implements IValidatedSet<K> {
|
||||
private readonly _wrappedSet: Set<K>;
|
||||
|
||||
public constructor();
|
||||
public constructor(iterable: Iterable<K>);
|
||||
public constructor(values?: ReadonlyArray<K>);
|
||||
public constructor(valuesOrIterable?: ReadonlyArray<K> | undefined | Iterable<K>) {
|
||||
this._wrappedSet = valuesOrIterable ? new Set(valuesOrIterable) : new Set();
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this._wrappedSet.size;
|
||||
}
|
||||
|
||||
public get [Symbol.toStringTag](): 'Set' {
|
||||
return 'ValidatedSet' as 'Set';
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._wrappedSet.clear();
|
||||
}
|
||||
|
||||
public delete(key: K): boolean {
|
||||
if (!this._wrappedSet.delete(key)) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Couldn't delete element with key ${key} because it wasn't present in the set`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public deleteIfExists(key: K): boolean {
|
||||
return this._wrappedSet.delete(key);
|
||||
}
|
||||
|
||||
public forEach(callbackfn: (key: K, sameKeyAgain: K, set: Set<K>) => void, thisArg?: any): void {
|
||||
this._wrappedSet.forEach(callbackfn, thisArg);
|
||||
}
|
||||
|
||||
public has(key: K): boolean {
|
||||
return this._wrappedSet.has(key);
|
||||
}
|
||||
|
||||
public add(key: K): this {
|
||||
if (this.has(key)) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(`Cannot add key ${key} because it already exists`);
|
||||
}
|
||||
|
||||
return this.addOrReplaceIfExists(key);
|
||||
}
|
||||
|
||||
public addOrReplaceIfExists(key: K): this {
|
||||
this._wrappedSet.add(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): IterableIterator<K> {
|
||||
return this._wrappedSet[Symbol.iterator]();
|
||||
}
|
||||
|
||||
public entries(): IterableIterator<[K, K]> {
|
||||
return this._wrappedSet.entries();
|
||||
}
|
||||
|
||||
public keys(): IterableIterator<K> {
|
||||
return this._wrappedSet.keys();
|
||||
}
|
||||
|
||||
public values(): IterableIterator<K> {
|
||||
return this._wrappedSet.values();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return printSet('ValidatedSet', this);
|
||||
}
|
||||
|
||||
public toArray(): K[] {
|
||||
return Array.from(this);
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { IEquivalenceComparable } from '../../utils/equivalence';
|
||||
|
||||
/**
|
||||
* These classes represents the different actions that a breakpoint can take when hit
|
||||
* Breakpoint: AlwaysPause
|
||||
* Conditional Breakpoint: ConditionalPause
|
||||
* Logpoint: LogMessage
|
||||
* Hit Count Breakpoint: PauseOnHitCount
|
||||
*/
|
||||
export interface IBPActionWhenHit extends IEquivalenceComparable {
|
||||
isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean;
|
||||
}
|
||||
|
||||
export class AlwaysPause implements IBPActionWhenHit {
|
||||
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
|
||||
return bpActionWhenHit instanceof AlwaysPause;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'always pause';
|
||||
}
|
||||
}
|
||||
|
||||
export class ConditionalPause implements IBPActionWhenHit {
|
||||
constructor(public readonly expressionOfWhenToPause: string) {}
|
||||
|
||||
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
|
||||
return (
|
||||
bpActionWhenHit instanceof ConditionalPause &&
|
||||
this.expressionOfWhenToPause === bpActionWhenHit.expressionOfWhenToPause
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `pause if: ${this.expressionOfWhenToPause}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class PauseOnHitCount implements IBPActionWhenHit {
|
||||
constructor(public readonly pauseOnHitCondition: string) {}
|
||||
|
||||
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
|
||||
return (
|
||||
bpActionWhenHit instanceof PauseOnHitCount &&
|
||||
this.pauseOnHitCondition === bpActionWhenHit.pauseOnHitCondition
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `pause when hits: ${this.pauseOnHitCondition}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class LogMessage implements IBPActionWhenHit {
|
||||
constructor(public readonly expressionToLog: string) {}
|
||||
|
||||
public isEquivalentTo(bpActionWhenHit: IBPActionWhenHit): boolean {
|
||||
return (
|
||||
bpActionWhenHit instanceof LogMessage &&
|
||||
this.expressionToLog === bpActionWhenHit.expressionToLog
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `log: ${this.expressionToLog}`;
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as Validation from '../../../validation';
|
||||
import { ColumnNumber, LineNumber, createLineNumber, createColumnNumber } from './subtypes';
|
||||
import { IEquivalenceComparable } from '../../utils/equivalence';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export type integer = number;
|
||||
|
||||
export class Position implements IEquivalenceComparable {
|
||||
public static readonly origin = new Position(createLineNumber(0), createColumnNumber(0));
|
||||
|
||||
constructor(public readonly lineNumber: LineNumber, public readonly columnNumber: ColumnNumber) {
|
||||
Validation.zeroOrPositive('Line number', lineNumber);
|
||||
if (columnNumber !== undefined) {
|
||||
Validation.zeroOrPositive('Column number', columnNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public static appearingLastOf(...positions: Position[]): Position {
|
||||
const lastPosition = _.reduce(positions, (left, right) =>
|
||||
left.doesAppearBefore(right) ? right : left,
|
||||
);
|
||||
if (lastPosition !== undefined) {
|
||||
return lastPosition;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Couldn't find the position appearing last from the list: ${positions}. Is it possible the list was empty?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static appearingFirstOf(...positions: Position[]): Position {
|
||||
const firstPosition = _.reduce(positions, (left, right) =>
|
||||
left.doesAppearBefore(right) ? left : right,
|
||||
);
|
||||
if (firstPosition !== undefined) {
|
||||
return firstPosition;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Couldn't find the position appearing first from the list: ${positions}. Is it possible the list was empty?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static isBetween(start: Position, maybeInBetween: Position, end: Position): boolean {
|
||||
return !maybeInBetween.doesAppearBefore(start) && !end.doesAppearBefore(maybeInBetween);
|
||||
}
|
||||
|
||||
public isEquivalentTo(location: Position): boolean {
|
||||
return this.lineNumber === location.lineNumber && this.columnNumber === location.columnNumber;
|
||||
}
|
||||
|
||||
public isOrigin(): boolean {
|
||||
return this.lineNumber === 0 && (this.columnNumber === undefined || this.columnNumber === 0);
|
||||
}
|
||||
|
||||
public doesAppearBefore(right: Position): boolean {
|
||||
return (
|
||||
this.lineNumber < right.lineNumber ||
|
||||
(this.lineNumber === right.lineNumber && this.columnNumber < right.columnNumber)
|
||||
);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.columnNumber !== undefined
|
||||
? `${this.lineNumber}:${this.columnNumber}`
|
||||
: `${this.lineNumber}`;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
// We use these types to have the compiler check that we are not sending a ColumnNumber where a LineNumber is expected
|
||||
|
||||
const lineIndexSymbol = Symbol();
|
||||
export type LineNumber = number & { [lineIndexSymbol]: true };
|
||||
|
||||
export function createLineNumber(numberRepresentation: number): LineNumber {
|
||||
return <LineNumber>numberRepresentation;
|
||||
}
|
||||
|
||||
const columnIndexSymbol = Symbol();
|
||||
export type ColumnNumber = number & { [columnIndexSymbol]: true };
|
||||
|
||||
export function createColumnNumber(numberRepresentation: number): ColumnNumber {
|
||||
return <ColumnNumber>numberRepresentation;
|
||||
}
|
||||
|
||||
const URLRegexpSymbol = Symbol();
|
||||
export type URLRegexp = string & { [URLRegexpSymbol]: true };
|
||||
|
||||
export function createURLRegexp(textRepresentation: string): URLRegexp {
|
||||
return <URLRegexp>textRepresentation;
|
||||
}
|
|
@ -1,252 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as _ from 'lodash';
|
||||
|
||||
enum Synchronicity {
|
||||
Sync,
|
||||
Async,
|
||||
}
|
||||
|
||||
enum Outcome {
|
||||
Succesful,
|
||||
Failure,
|
||||
}
|
||||
|
||||
export class ReplacementInstruction {
|
||||
public constructor(public readonly pattern: RegExp, public readonly replacement: string) {}
|
||||
}
|
||||
|
||||
export interface IMethodsCalledLoggerConfiguration {
|
||||
readonly replacements: ReplacementInstruction[];
|
||||
|
||||
customizeResult(methodName: string | symbol | number, args: unknown[], result: unknown): unknown;
|
||||
customizeArgumentsBeforeCall(
|
||||
receiverName: string,
|
||||
methodName: string | symbol | number,
|
||||
args: unknown[],
|
||||
): void;
|
||||
}
|
||||
|
||||
export class MethodsCalledLoggerConfiguration implements IMethodsCalledLoggerConfiguration {
|
||||
public constructor(
|
||||
public readonly containerName: string,
|
||||
private _replacements: ReplacementInstruction[],
|
||||
) {}
|
||||
|
||||
public customizeResult(
|
||||
_methodName: string | symbol | number,
|
||||
_args: unknown[],
|
||||
result: unknown,
|
||||
): unknown {
|
||||
return result;
|
||||
}
|
||||
|
||||
public customizeArgumentsBeforeCall(
|
||||
receiverName: string,
|
||||
methodName: string | symbol | number,
|
||||
args: object[],
|
||||
): void {
|
||||
if (methodName === 'on' && args.length >= 2) {
|
||||
args[1] = new MethodsCalledLogger(
|
||||
this,
|
||||
args[1],
|
||||
`(${receiverName} emits ${args[0]})`,
|
||||
).wrapped();
|
||||
}
|
||||
}
|
||||
|
||||
public get replacements(): ReplacementInstruction[] {
|
||||
return this._replacements;
|
||||
}
|
||||
|
||||
public updateReplacements(replacements: ReplacementInstruction[]): void {
|
||||
this._replacements = replacements;
|
||||
}
|
||||
}
|
||||
|
||||
export class MethodsCalledLogger<T extends object> {
|
||||
private static _nextCallId = 10000;
|
||||
constructor(
|
||||
private readonly _configuration: IMethodsCalledLoggerConfiguration,
|
||||
private readonly _objectToWrap: T,
|
||||
private readonly _objectToWrapName: string,
|
||||
) {}
|
||||
|
||||
public wrapped(): T {
|
||||
const handler = {
|
||||
get: <K extends keyof T>(target: T, propertyKey: K, receiver: any) => {
|
||||
const originalPropertyValue = target[propertyKey];
|
||||
if (typeof originalPropertyValue === 'function') {
|
||||
return (...args: any) => {
|
||||
const callId = this.generateCallId();
|
||||
try {
|
||||
this.logCallStart(propertyKey, args, callId);
|
||||
this._configuration.customizeArgumentsBeforeCall(
|
||||
this._objectToWrapName,
|
||||
propertyKey,
|
||||
args,
|
||||
);
|
||||
const result = originalPropertyValue.apply(target, args);
|
||||
if (!result || !result.then) {
|
||||
this.logCall(
|
||||
propertyKey,
|
||||
Synchronicity.Sync,
|
||||
args,
|
||||
Outcome.Succesful,
|
||||
result,
|
||||
callId,
|
||||
);
|
||||
if (result === target) {
|
||||
return receiver;
|
||||
} else {
|
||||
return this._configuration.customizeResult(propertyKey, args, result);
|
||||
}
|
||||
} else {
|
||||
this.logSyncPartFinished(propertyKey, args, callId);
|
||||
return result.then(
|
||||
(promiseResult: unknown) => {
|
||||
this.logCall(
|
||||
propertyKey,
|
||||
Synchronicity.Async,
|
||||
args,
|
||||
Outcome.Succesful,
|
||||
promiseResult,
|
||||
callId,
|
||||
);
|
||||
if (promiseResult === target) {
|
||||
return receiver;
|
||||
} else {
|
||||
return this._configuration.customizeResult(propertyKey, args, promiseResult);
|
||||
}
|
||||
},
|
||||
(error: unknown) => {
|
||||
this.logCall(
|
||||
propertyKey,
|
||||
Synchronicity.Async,
|
||||
args,
|
||||
Outcome.Failure,
|
||||
error,
|
||||
callId,
|
||||
);
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (exception) {
|
||||
this.logCall(
|
||||
propertyKey,
|
||||
Synchronicity.Sync,
|
||||
args,
|
||||
Outcome.Failure,
|
||||
exception,
|
||||
callId,
|
||||
);
|
||||
throw exception;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return originalPropertyValue;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return new Proxy<T>(this._objectToWrap, handler);
|
||||
}
|
||||
|
||||
private generateCallId(): number {
|
||||
return MethodsCalledLogger._nextCallId++;
|
||||
}
|
||||
|
||||
// private printMethodCall(propertyKey: PropertyKey, methodCallArguments: any[]): string {
|
||||
// return `${this._objectToWrapName}.${String(propertyKey)}(${this.printArguments(methodCallArguments)})`;
|
||||
// }
|
||||
|
||||
// private printMethodResponse(outcome: Outcome, resultOrException: unknown): string {
|
||||
// return `${outcome === Outcome.Succesful ? '->' : 'threw'} ${this.printObject(resultOrException)}`;
|
||||
// }
|
||||
|
||||
// private printMethodSynchronicity(synchronicity: Synchronicity): string {
|
||||
// return `${synchronicity === Synchronicity.Sync ? '' : ' async'}`;
|
||||
// }
|
||||
|
||||
/** Returns the test file and line that the code is currently executing e.g.:
|
||||
* < >
|
||||
* [22:23:28.468 UTC] START 10026: hitCountBreakpointTests.test.ts:34:2 | #incrementBtn.click()
|
||||
*/
|
||||
// TODO: Figure out how to integrate this with V2. We don't want to do this for production logging because new Error().stack is slow
|
||||
// private getTestFileAndLine(): string {
|
||||
// const stack = new Error().stack;
|
||||
// if (stack) {
|
||||
// const stackLines = stack.split('\n');
|
||||
// const testCaseLine = stackLines.find(line => line.indexOf('test.ts') >= 0);
|
||||
// if (testCaseLine) {
|
||||
// const filenameAndLine = testCaseLine.lastIndexOf(path.sep);
|
||||
// if (filenameAndLine >= 0) {
|
||||
// const fileNameAndLineNumber = testCaseLine.substring(filenameAndLine + 1, testCaseLine.length - 2);
|
||||
// return `${fileNameAndLineNumber} | `;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return '';
|
||||
// }
|
||||
|
||||
private logCallStart(
|
||||
_propertyKey: PropertyKey,
|
||||
_methodCallArguments: any[],
|
||||
_callId: number,
|
||||
): void {
|
||||
// const getTestFileAndLine = this.getTestFileAndLine();
|
||||
// const message = `START ${callId}: ${getTestFileAndLine}${this.printMethodCall(propertyKey, methodCallArguments)}`;
|
||||
// logger.verbose(message);
|
||||
}
|
||||
|
||||
private logSyncPartFinished(
|
||||
_propertyKey: PropertyKey,
|
||||
_methodCallArguments: any[],
|
||||
_callId: number,
|
||||
): void {
|
||||
// const getTestFileAndLine = this.getTestFileAndLine();
|
||||
// const message = `PROMISE-RETURNED ${callId}: ${getTestFileAndLine}${this.printMethodCall(propertyKey, methodCallArguments)}`;
|
||||
// logger.verbose(message);
|
||||
}
|
||||
|
||||
private logCall(
|
||||
_propertyKey: PropertyKey,
|
||||
_synchronicity: Synchronicity,
|
||||
_methodCallArguments: any[],
|
||||
_outcome: Outcome,
|
||||
_resultOrException: unknown,
|
||||
_callId: number,
|
||||
): void {
|
||||
// const endPrefix = callId ? `END ${callId}: ` : '';
|
||||
// const message = `${endPrefix}${this.printMethodCall(propertyKey, methodCallArguments)} ${this.printMethodSynchronicity(synchronicity)} ${this.printMethodResponse(outcome, resultOrException)}`;
|
||||
// logger.verbose(message);
|
||||
}
|
||||
|
||||
// private printArguments(methodCallArguments: any[]): string {
|
||||
// return methodCallArguments.map(methodCallArgument => this.printObject(methodCallArgument)).join(', ');
|
||||
// }
|
||||
|
||||
// private printObject(objectToPrint: unknown): string {
|
||||
// const description = printTopLevelObjectDescription(objectToPrint);
|
||||
// const printedReduced = _.reduce(Array.from(this._configuration.replacements),
|
||||
// (text, replacement) =>
|
||||
// text.replace(replacement.pattern, replacement.replacement),
|
||||
// description);
|
||||
|
||||
// return printedReduced;
|
||||
// }
|
||||
}
|
||||
|
||||
export function wrapWithMethodLogger<T extends object>(
|
||||
objectToWrap: T,
|
||||
objectToWrapName = `${objectToWrap}`,
|
||||
): T {
|
||||
return new MethodsCalledLogger(
|
||||
new MethodsCalledLoggerConfiguration('no container', []),
|
||||
objectToWrap,
|
||||
objectToWrapName,
|
||||
).wrapped();
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export function printTopLevelObjectDescription(objectToPrint: unknown) {
|
||||
return printObjectDescription(objectToPrint, printFirstLevelProperties);
|
||||
}
|
||||
|
||||
export function printObjectDescription(
|
||||
objectToPrint: unknown,
|
||||
fallbackPrintDescription = (obj: unknown) => `${obj}`,
|
||||
) {
|
||||
let printed = `<logic to print this object doesn't exist>`;
|
||||
if (!objectToPrint) {
|
||||
printed = `${objectToPrint}`;
|
||||
} else if (typeof objectToPrint === 'object') {
|
||||
// Proxies throw an exception when toString is called, so we need to check this first
|
||||
if (typeof (<any>objectToPrint).on === 'function') {
|
||||
// This is a noice-json-rpc proxy
|
||||
printed = 'CDTP Proxy';
|
||||
} else {
|
||||
// This if is actually unnecesary, the previous if (!objectToPrint) { does the same thing. For some reason the typescript compiler cannot infer the type from that if
|
||||
// so we just write this code to leave the compiler happy
|
||||
// TODO: Sync with the typescript team and figure out how to remove this
|
||||
if (!objectToPrint) {
|
||||
printed = `${objectToPrint}`;
|
||||
} else {
|
||||
const toString = objectToPrint.toString();
|
||||
if (toString !== '[object Object]') {
|
||||
printed = toString;
|
||||
} else if (isJSONObject(objectToPrint)) {
|
||||
printed = JSON.stringify(objectToPrint);
|
||||
} else if (objectToPrint.constructor === Object) {
|
||||
printed = fallbackPrintDescription(objectToPrint);
|
||||
} else {
|
||||
printed = `${objectToPrint}(${objectToPrint.constructor.name})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof objectToPrint === 'function') {
|
||||
if (objectToPrint.name) {
|
||||
printed = objectToPrint.name;
|
||||
} else {
|
||||
const functionSourceCode = objectToPrint.toString();
|
||||
|
||||
// Find param => or (param1, param2)
|
||||
const parenthesisIndex = _.findIndex(
|
||||
functionSourceCode,
|
||||
character => character === ')' || character === '=',
|
||||
);
|
||||
const functionParameters = functionSourceCode.substr(
|
||||
functionSourceCode[0] === '(' ? 1 : 0,
|
||||
parenthesisIndex - 1,
|
||||
);
|
||||
printed = `Anonymous function: ${functionParameters}`;
|
||||
}
|
||||
} else {
|
||||
printed = `${objectToPrint}`;
|
||||
}
|
||||
|
||||
return printed;
|
||||
}
|
||||
|
||||
function isJSONObject(objectToPrint: any): boolean {
|
||||
if (objectToPrint.constructor === Object) {
|
||||
const values = _.values(objectToPrint);
|
||||
return values.every(value => !value || value.constructor === Object);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function printFirstLevelProperties(objectToPrint: any): string {
|
||||
const printedProeprties = Object.keys(objectToPrint).map(
|
||||
key => `${key}: ${printObjectDescription(objectToPrint[key])}`,
|
||||
);
|
||||
return `{ ${printedProeprties.join(', ')} }`;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
export interface IEquivalenceComparable {
|
||||
isEquivalentTo(right: this): boolean;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Type utilities to construct derived types from the original types, rather than have to manually write them
|
||||
*/
|
||||
export type MakePropertyRequired<T, K extends keyof T> = T & { [P in K]-?: T[K] };
|
||||
export type RemoveProperty<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
export type SpecializeProperty<T, K extends keyof T, S extends T[K]> = T & { [P in K]: S };
|
||||
|
||||
export function isNotUndefined<T>(object: T | undefined): object is T {
|
||||
return object !== undefined;
|
||||
}
|
||||
|
||||
export interface Array<T> {
|
||||
filter<U extends T>(predicate: (element: T) => element is U): U[];
|
||||
}
|
||||
|
||||
export type Replace<T, R extends keyof T, N> = {
|
||||
[K in keyof T]: K extends R ? N : T[K];
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
export function zeroOrPositive(name: string, value: number) {
|
||||
if (value < 0) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Expected ${name} to be either zero or a positive number and instead it was ${value}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Used for debugging while developing to automatically break when something unexpected happened */
|
||||
export function breakWhileDebugging() {
|
||||
if (process.env.BREAK_WHILE_DEBUGGING === 'true') {
|
||||
// tslint:disable-next-line:no-debugger
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
|
||||
export function notNullNorUndefinedElements(name: string, array: unknown[]): void {
|
||||
const index = array.findIndex(element => element === null || element === undefined);
|
||||
if (index >= 0) {
|
||||
breakWhileDebugging();
|
||||
throw new Error(
|
||||
`Expected ${name} to not have any null or undefined elements, yet the element at #${index} was ${array[index]}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { isWindows } from '../testSetup';
|
||||
import { onUnhandledException } from '../utils/onUnhandledException';
|
||||
import * as testUtils from '../testUtils';
|
||||
|
||||
const testSpec = TestProjectSpec.fromTestPath('featuresTests/attachNoUrl');
|
||||
|
||||
let waitForOutput: testUtils.IDeferred<void>;
|
||||
|
||||
// TODO: The attach test is currently failing on MAC. We need to investigate it and fix it
|
||||
((isWindows ? testUsing : testUsing.skip) as typeof testUsing)(
|
||||
'Attach without specifying an url parameter',
|
||||
async context => {
|
||||
waitForOutput = await testUtils.getDeferred();
|
||||
return LaunchProject.attach(context, testSpec, undefined, {
|
||||
registerListeners: client => {
|
||||
// This test tests 2 different things while attaching:
|
||||
// 1. We don't get an unhandled error while attaching (due to Runtime.consoleAPICalled being called with a scriptId that hasn't been parsed yet)
|
||||
onUnhandledException(client, exceptionMessage =>
|
||||
waitForOutput.reject(new Error(exceptionMessage)),
|
||||
);
|
||||
|
||||
client.on('output', (args: DebugProtocol.OutputEvent) => {
|
||||
// 2. We eventually see this console.log message, because we attached succesfully to the web-page
|
||||
if (
|
||||
args.body.category === 'stdout' &&
|
||||
args.body.output.startsWith('If you see this message, you are attached...')
|
||||
) {
|
||||
// Wait 1 second to see if any unhandled errors happen while attaching to the page
|
||||
testUtils.promiseTimeout(undefined, 1000).then(() => {
|
||||
waitForOutput.resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
async () => {
|
||||
await waitForOutput.promise;
|
||||
},
|
||||
);
|
|
@ -1,24 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
|
||||
testUsing.skip(
|
||||
'Hit breakpoint on JavaScript when source map is invalid',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('featuresTests/invalidSourceMap')),
|
||||
async launchProject => {
|
||||
const runCodeButton = await launchProject.page.waitForSelector('#runCode');
|
||||
const breakpoint = await launchProject.breakpoints
|
||||
.at('../app.js')
|
||||
.breakpoint({ text: `console.log('line 5');` });
|
||||
|
||||
await breakpoint.assertIsHitThenResumeWhen(() => runCodeButton.click(), {
|
||||
stackTrace: `
|
||||
runCode [app.js] Line 11:5 // Because the source-map is invalid we hit in app.js:11:5 instead of app.ts:5`,
|
||||
});
|
||||
},
|
||||
);
|
|
@ -1,38 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { readFileP, writeFileP } from '../testUtils';
|
||||
|
||||
suite('Unusual source-maps', () => {
|
||||
testUsing(
|
||||
`file:/// url in sources' field`,
|
||||
async context => {
|
||||
const testSpec = TestProjectSpec.fromTestPath(
|
||||
'featuresTests/unusualSourceMaps/fileUrlInSources',
|
||||
);
|
||||
|
||||
// Update source-map to have a file:/// url in the sources field
|
||||
const sourceMapPath = testSpec.src('../app.js.map');
|
||||
const sourceMapContents = await readFileP(sourceMapPath);
|
||||
const sourceMapJSON = JSON.parse(sourceMapContents.toString());
|
||||
sourceMapJSON['sources'] = [`file:///${testSpec.src('../app.ts').replace(/\\/g, '/')}`];
|
||||
await writeFileP(sourceMapPath, JSON.stringify(sourceMapJSON));
|
||||
|
||||
return LaunchProject.launch(context, testSpec);
|
||||
},
|
||||
async launchProject => {
|
||||
const executeActionButton = await launchProject.page.waitForSelector('#executeAction');
|
||||
|
||||
const buttonClickedBreakpoint = await launchProject.breakpoints
|
||||
.at('../app.ts')
|
||||
.breakpoint({ text: `console.log('You clicked the button');` });
|
||||
|
||||
await buttonClickedBreakpoint.assertIsHitThenResumeWhen(() => executeActionButton.click());
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,150 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Hit count breakpoints' scenarios
|
||||
* Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
|
||||
import { reactTestSpecification } from '../resources/resourceProjects';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
import { asyncRepeatSerially } from '../utils/repeat';
|
||||
|
||||
puppeteerSuite.skip(
|
||||
'Hit count breakpoints on a React project',
|
||||
reactTestSpecification,
|
||||
suiteContext => {
|
||||
puppeteerTest(
|
||||
"Hit count breakpoint = 3 pauses on the button's 3rd click",
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'this.setState({ count: newval });',
|
||||
boundText: 'setState({ count: newval })',
|
||||
hitCountCondition: '% 3',
|
||||
});
|
||||
|
||||
await asyncRepeatSerially(2, () => incBtn.click());
|
||||
|
||||
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
|
||||
await incBtn.click();
|
||||
|
||||
await breakpoints.waitAndAssertNoMoreEvents();
|
||||
|
||||
await setStateBreakpoint.unset();
|
||||
},
|
||||
);
|
||||
|
||||
puppeteerTest(
|
||||
"Hit count breakpoints = 3, = 4 and = 5 pause on the button's 3rd, 4th and 5th clicks",
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'this.setState({ count: newval })',
|
||||
boundText: 'setState({ count: newval })',
|
||||
hitCountCondition: '= 3',
|
||||
});
|
||||
|
||||
const setNewValBreakpoint = await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'const newval = this.state.count + 1',
|
||||
boundText: 'state.count + 1',
|
||||
hitCountCondition: '= 5',
|
||||
});
|
||||
|
||||
const stepInBreakpoint = await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'this.stepIn()',
|
||||
boundText: 'stepIn()',
|
||||
hitCountCondition: '= 4',
|
||||
});
|
||||
|
||||
await asyncRepeatSerially(2, () => incBtn.click());
|
||||
|
||||
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
|
||||
await incBtn.click();
|
||||
|
||||
await breakpoints.waitAndAssertNoMoreEvents();
|
||||
|
||||
await setStateBreakpoint.unset();
|
||||
await setNewValBreakpoint.unset();
|
||||
await stepInBreakpoint.unset();
|
||||
},
|
||||
);
|
||||
|
||||
puppeteerTest(
|
||||
"Hit count breakpoints = 3, = 4 and = 5 set in batch pause on the button's 3rd, 4th and 5th clicks",
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
const {
|
||||
setStateBreakpoint,
|
||||
stepInBreakpoint,
|
||||
setNewValBreakpoint,
|
||||
} = await counterBreakpoints.batch(async () => ({
|
||||
setStateBreakpoint: await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'this.setState({ count: newval });',
|
||||
boundText: 'setState({ count: newval })',
|
||||
hitCountCondition: '= 3',
|
||||
}),
|
||||
|
||||
setNewValBreakpoint: await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'const newval = this.state.count + 1',
|
||||
boundText: 'state.count + 1',
|
||||
hitCountCondition: '= 5',
|
||||
}),
|
||||
|
||||
stepInBreakpoint: await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'this.stepIn();',
|
||||
boundText: 'stepIn()',
|
||||
hitCountCondition: '= 4',
|
||||
}),
|
||||
}));
|
||||
|
||||
await asyncRepeatSerially(2, () => incBtn.click());
|
||||
|
||||
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
await stepInBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
await setNewValBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click());
|
||||
|
||||
await incBtn.click();
|
||||
|
||||
await breakpoints.waitAndAssertNoMoreEvents();
|
||||
|
||||
await counterBreakpoints.batch(async () => {
|
||||
await setStateBreakpoint.unset();
|
||||
await setNewValBreakpoint.unset();
|
||||
await stepInBreakpoint.unset();
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
|
@ -1,195 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Hit count breakpoints' scenarios
|
||||
* Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
|
||||
import { reactWithLoopTestSpecification } from '../resources/resourceProjects';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
|
||||
puppeteerSuite(
|
||||
'Hit count breakpoints combinations',
|
||||
reactWithLoopTestSpecification,
|
||||
suiteContext => {
|
||||
interface IConditionConfiguration {
|
||||
condition: string; // The condition for the hit count breakpoint
|
||||
iterationsExpectedToPause: number[]; // In which iteration numbers it should pause (e.g.: 1st, 5th, 12th, etc...)
|
||||
noMorePausesAfterwards: boolean;
|
||||
}
|
||||
|
||||
// * Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
|
||||
const manyConditionsConfigurations: IConditionConfiguration[] = [
|
||||
{ condition: '= 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
|
||||
{ condition: '= 1', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
|
||||
{ condition: '= 2', iterationsExpectedToPause: [2], noMorePausesAfterwards: true },
|
||||
{ condition: '= 12', iterationsExpectedToPause: [12], noMorePausesAfterwards: true },
|
||||
{
|
||||
condition: '> 0',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '> 1',
|
||||
iterationsExpectedToPause: [2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '>\t2',
|
||||
iterationsExpectedToPause: [3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '> 187',
|
||||
iterationsExpectedToPause: [188, 189, 190, 191],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '>= 0',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '>= 1',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '>= 2',
|
||||
iterationsExpectedToPause: [2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '>= 37',
|
||||
iterationsExpectedToPause: [37, 38, 39],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{ condition: '< 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
|
||||
{ condition: '< \t \t 1', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
|
||||
{ condition: '< 2', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
|
||||
{
|
||||
condition: '< \t13',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
||||
noMorePausesAfterwards: true,
|
||||
},
|
||||
{ condition: '<=\t 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
|
||||
{ condition: '<= 1', iterationsExpectedToPause: [1], noMorePausesAfterwards: true },
|
||||
{
|
||||
condition: '<= 15',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
noMorePausesAfterwards: true,
|
||||
},
|
||||
{ condition: '% 0', iterationsExpectedToPause: [], noMorePausesAfterwards: true },
|
||||
{
|
||||
condition: '% 1',
|
||||
iterationsExpectedToPause: [1, 2, 3, 4, 5, 6],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '% 2',
|
||||
iterationsExpectedToPause: [2, 4, 6, 8, 10],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '%\t3',
|
||||
iterationsExpectedToPause: [3, 6, 9, 12, 15],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '% \t \t \t 12',
|
||||
iterationsExpectedToPause: [12, 24, 36, 48, 60],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '%\t\t\t17',
|
||||
iterationsExpectedToPause: [17, 34, 51, 68],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
{
|
||||
condition: '% 37',
|
||||
iterationsExpectedToPause: [37, 74, 111, 148],
|
||||
noMorePausesAfterwards: false,
|
||||
},
|
||||
];
|
||||
|
||||
manyConditionsConfigurations.forEach(conditionConfiguration => {
|
||||
puppeteerTest.skip(
|
||||
`condition ${conditionConfiguration.condition}`,
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactWithLoopTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
const setStateBreakpoint = await counterBreakpoints.hitCountBreakpoint({
|
||||
text: 'iterationNumber * iterationNumber',
|
||||
hitCountCondition: conditionConfiguration.condition,
|
||||
});
|
||||
|
||||
const buttonClicked = incBtn.click();
|
||||
|
||||
for (const nextIterationToPause of conditionConfiguration.iterationsExpectedToPause) {
|
||||
/**
|
||||
* The iterationNumber variable counts in the js-debuggee code how many times the loop was executed. We verify
|
||||
* the value of this variable to validate that a bp with = 12 paused on the 12th iteration rather than on the 1st one
|
||||
* (The breakpoint is located in the same place in both iterations, so we need to use state to differenciate between those two cases)
|
||||
*/
|
||||
await setStateBreakpoint.assertIsHitThenResume({
|
||||
variables: { local_contains: { iterationNumber: nextIterationToPause } },
|
||||
});
|
||||
}
|
||||
|
||||
// logger.log(`No more pauses afterwards = ${conditionConfiguration.noMorePausesAfterwards}`); // TODO@rob
|
||||
if (conditionConfiguration.noMorePausesAfterwards) {
|
||||
await breakpoints.waitAndAssertNoMoreEvents();
|
||||
await setStateBreakpoint.unset();
|
||||
} else {
|
||||
await breakpoints.waitAndConsumePausedEvent(setStateBreakpoint);
|
||||
await setStateBreakpoint.unset();
|
||||
await breakpoints.resume();
|
||||
}
|
||||
|
||||
await buttonClicked;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// * Hit count breakpoint syntax: (>|>=|=|<|<=|%)?\s*([0-9]+)
|
||||
const manyInvalidConditions: string[] = [
|
||||
'== 3',
|
||||
'= -1',
|
||||
'> -200',
|
||||
'< -24',
|
||||
'>= -95',
|
||||
'<= -5',
|
||||
'< = 4',
|
||||
'% -200',
|
||||
'stop always',
|
||||
'= 1 + 1',
|
||||
'> 3.5',
|
||||
];
|
||||
|
||||
manyInvalidConditions.forEach(invalidCondition => {
|
||||
puppeteerTest.skip(`invalid condition ${invalidCondition}`, suiteContext, async () => {
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactWithLoopTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
await counterBreakpoints.unverifiedHitCountBreakpoint({
|
||||
text: 'iterationNumber * iterationNumber',
|
||||
hitCountCondition: invalidCondition,
|
||||
unverifiedReason: `Didn't recognize <${invalidCondition}> as a valid hit count condition`,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
|
@ -1,45 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as testSetup from '../testSetup';
|
||||
import { puppeteerSuite } from '../puppeteer/puppeteerSuite';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { FrameworkTestSuite } from '../framework/frameworkCommonTests';
|
||||
import * as path from 'path';
|
||||
import { pathToFileURL } from '../testUtils';
|
||||
|
||||
const SINGLE_INLINE_TEST_SPEC = TestProjectSpec.fromTestPath(
|
||||
'inline_scripts',
|
||||
'',
|
||||
pathToFileURL(path.join(testSetup.DATA_ROOT, 'inline_scripts/single.html')),
|
||||
);
|
||||
const MULTIPLE_INLINE_TEST_SPEC = TestProjectSpec.fromTestPath(
|
||||
'inline_scripts',
|
||||
'',
|
||||
pathToFileURL(path.join(testSetup.DATA_ROOT, 'inline_scripts/multiple.html')),
|
||||
);
|
||||
|
||||
suite('Inline Script Tests', () => {
|
||||
puppeteerSuite('Single inline script', SINGLE_INLINE_TEST_SPEC, suiteContext => {
|
||||
const frameworkTests = new FrameworkTestSuite('Simple JS', suiteContext);
|
||||
frameworkTests.testBreakpointHitsOnPageAction(
|
||||
'Should stop on a breakpoint in an in-line script',
|
||||
'#actionButton',
|
||||
'single.html',
|
||||
'a + b;',
|
||||
page => page.click('#actionButton'),
|
||||
);
|
||||
});
|
||||
|
||||
puppeteerSuite.skip('Multiple inline scripts', MULTIPLE_INLINE_TEST_SPEC, suiteContext => {
|
||||
const frameworkTests = new FrameworkTestSuite('Simple JS', suiteContext);
|
||||
frameworkTests.testBreakpointHitsOnPageAction(
|
||||
'Should stop on a breakpoint in multiple in-line scripts (Skipped, not currently working in V2)',
|
||||
'#actionButton',
|
||||
'multiple.html',
|
||||
'inlineScript1',
|
||||
page => page.click('#actionButton'),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { onUnhandledException, onHandledError } from '../utils/onUnhandledException';
|
||||
import { promiseTimeout, getDeferred, IDeferred } from '../testUtils';
|
||||
import { IChromeLaunchConfiguration } from '../../configuration';
|
||||
|
||||
let waitForTestResult: IDeferred<void>;
|
||||
|
||||
testUsing(
|
||||
'No unhandled exceptions when we parse invalid JavaScript code. We get a handled error',
|
||||
async context => {
|
||||
waitForTestResult = await getDeferred<void>();
|
||||
return LaunchProject.launch(
|
||||
context,
|
||||
TestProjectSpec.fromTestPath('featuresTests/invalidJavaScriptCode'),
|
||||
{} as IChromeLaunchConfiguration,
|
||||
{
|
||||
registerListeners: client => {
|
||||
// We fail the test if we get an unhandled exception
|
||||
onUnhandledException(client, exceptionMessage =>
|
||||
waitForTestResult.reject(new Error(exceptionMessage)),
|
||||
);
|
||||
// We expect to get a handled error instead
|
||||
onHandledError(client, async errorMessage => {
|
||||
if (errorMessage.startsWith(`SyntaxError: Unexpected token 'function'`)) {
|
||||
// After we get the message, we wait 1 more second to verify we don't get any unhandled exceptions, and then we succeed the test
|
||||
await promiseTimeout(undefined, 1000 /* 1 sec */);
|
||||
|
||||
waitForTestResult.resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
async _launchProject => {
|
||||
await waitForTestResult.promise;
|
||||
},
|
||||
);
|
|
@ -1,117 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { fail } from 'assert';
|
||||
import { expect } from 'chai';
|
||||
import { IChromeLaunchConfiguration } from '../../configuration';
|
||||
import { readFileP } from '../testUtils';
|
||||
|
||||
let loadedSources: DebugProtocol.Source[] = [];
|
||||
|
||||
function onLoadedSource(args: DebugProtocol.LoadedSourceEvent): void {
|
||||
switch (args.body.reason) {
|
||||
case 'new':
|
||||
// We ignore scripts added by puppeteer
|
||||
if (args.body.source.name !== '__puppeteer_evaluation_script__') {
|
||||
loadedSources.push(args.body.source);
|
||||
}
|
||||
break;
|
||||
case 'changed':
|
||||
case 'removed':
|
||||
fail(`Only expected new loaded source events`);
|
||||
break;
|
||||
default:
|
||||
fail(`Unrecognized loaded source reason: ${args.body.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
suite('loaded sources', () => {
|
||||
setup(() => {
|
||||
loadedSources = []; // Reset before each test
|
||||
});
|
||||
|
||||
const testSpec = TestProjectSpec.fromTestPath('featuresTests/loadedSources/basicLoadedSources');
|
||||
testUsing(
|
||||
'we receive events for js, ts, and eval sources',
|
||||
context =>
|
||||
LaunchProject.launch(
|
||||
context,
|
||||
testSpec,
|
||||
{} as IChromeLaunchConfiguration, // TODO@rob
|
||||
{
|
||||
registerListeners: client =>
|
||||
client.on('loadedSource', args =>
|
||||
onLoadedSource(<DebugProtocol.LoadedSourceEvent>args),
|
||||
),
|
||||
},
|
||||
),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
expect(loadedSources.length).to.equal(4);
|
||||
expect(loadedSources[0].name).to.equal('app.js');
|
||||
expect(loadedSources[0].path).to.match(new RegExp('http://localhost:[0-9]+/app.js'));
|
||||
|
||||
expect(loadedSources[1].name).to.match(/VM[0-9]+/);
|
||||
expect(loadedSources[1].path).to.match(/<eval>\\VM[0-9]+/);
|
||||
|
||||
expect(loadedSources[2].name).to.equal('jsUtilities.js');
|
||||
expect(loadedSources[2].path).to.match(new RegExp('http://localhost:[0-9]+/jsUtilities.js'));
|
||||
|
||||
// These are the 2 inline scripts in the .html file
|
||||
expect(loadedSources[3].name).to.match(new RegExp('localhost:[0-9]+'));
|
||||
expect(loadedSources[3].path).to.match(new RegExp('http://localhost:[0-9]+'));
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'can get dynamic JavaScript file source',
|
||||
context =>
|
||||
LaunchProject.launch(context, testSpec, {} as IChromeLaunchConfiguration, {
|
||||
registerListeners: client =>
|
||||
client.on('loadedSource', args => onLoadedSource(<DebugProtocol.LoadedSourceEvent>args)),
|
||||
}),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
expect(loadedSources[0].name).to.equal('app.js');
|
||||
const contents = await launchProject.debugClient.sourceRequest({
|
||||
source: { sourceReference: loadedSources[0].sourceReference },
|
||||
sourceReference: 0 /** Not used. Backwards compatibility */,
|
||||
});
|
||||
expect(contents.success).to.equal(true);
|
||||
|
||||
const appFileContents = await readFileP(testSpec.src('../app.js'));
|
||||
expect(contents.body.content).to.equal(appFileContents);
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'can get dynamic .html file source',
|
||||
context =>
|
||||
LaunchProject.launch(context, testSpec, {} as IChromeLaunchConfiguration, {
|
||||
registerListeners: client =>
|
||||
client.on('loadedSource', args => onLoadedSource(<DebugProtocol.LoadedSourceEvent>args)),
|
||||
}),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
// We need to finish loading the .html file, so we can request it's source content
|
||||
await launchProject.pausedWizard.resume();
|
||||
|
||||
expect(loadedSources[3].name).to.match(new RegExp('localhost:[0-9]+'));
|
||||
const contents = await launchProject.debugClient.sourceRequest({
|
||||
source: { sourceReference: loadedSources[3].sourceReference },
|
||||
sourceReference: 0 /** Not used. Backwards compatibility */,
|
||||
});
|
||||
expect(contents.success).to.equal(true);
|
||||
|
||||
const appFileContents = await readFileP(testSpec.src('../index.html'));
|
||||
expect(contents.body.content).to.equal(appFileContents);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { VariablesWizard } from '../wizards/variables/variablesWizard';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
|
||||
suite('modify variable', function() {
|
||||
const testSpec = TestProjectSpec.fromTestPath('featuresTests/setVariable');
|
||||
testUsing(
|
||||
'local',
|
||||
context => LaunchProject.launch(context, testSpec),
|
||||
async launchProject => {
|
||||
const variables = new VariablesWizard(launchProject.debugClient);
|
||||
const breakpoints = BreakpointsWizard.create(launchProject.debugClient, testSpec).at(
|
||||
'../app.ts',
|
||||
);
|
||||
const changeShouldExitBreakpoint = await breakpoints.breakpoint({
|
||||
text: `console.log('Change shouldExit value here')`,
|
||||
});
|
||||
const exitedPreviousFunctionBreakpoint = await breakpoints.breakpoint({
|
||||
text: `console.log('We exited the previous function');`,
|
||||
});
|
||||
|
||||
await changeShouldExitBreakpoint.assertIsHitThenResume({
|
||||
action: async () => {
|
||||
await variables.set('shouldExit', 'true');
|
||||
},
|
||||
});
|
||||
|
||||
await exitedPreviousFunctionBreakpoint.assertIsHitThenResume({});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,59 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
|
||||
|
||||
import { reactTestSpecification } from '../resources/resourceProjects';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
|
||||
puppeteerSuite('Multiple breakpoints on a React project', reactTestSpecification, suiteContext => {
|
||||
puppeteerTest.skip(
|
||||
'Can hit two valid breakpoints, while we set them with an invalid hit count breakpoints',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
const breakpoints = BreakpointsWizard.create(
|
||||
suiteContext.debugClient,
|
||||
reactTestSpecification,
|
||||
);
|
||||
const counterBreakpoints = breakpoints.at('Counter.jsx');
|
||||
|
||||
const { setStateBreakpoint, setNewValBreakpoint } = await counterBreakpoints.batch(
|
||||
async () => ({
|
||||
stepInBreakpoint: await (
|
||||
await counterBreakpoints.unsetHitCountBreakpoint({
|
||||
text: 'this.stepIn();',
|
||||
boundText: 'stepIn()',
|
||||
hitCountCondition: 'bad bad hit count breakpoint condition = 2',
|
||||
})
|
||||
).setWithoutVerifying(), // We want the invalid condition to be first to see that the other 2 breakpoints are actually set
|
||||
|
||||
setNewValBreakpoint: await counterBreakpoints.breakpoint({
|
||||
text: 'const newval = this.state.count + 1',
|
||||
boundText: 'state.count + 1',
|
||||
}),
|
||||
|
||||
setStateBreakpoint: await counterBreakpoints.breakpoint({
|
||||
text: 'this.setState({ count: newval });',
|
||||
boundText: 'setState({ count: newval })',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await breakpoints.assertIsHitThenResumeWhen(
|
||||
[setNewValBreakpoint, setStateBreakpoint],
|
||||
() => incBtn.click(),
|
||||
{},
|
||||
);
|
||||
|
||||
await breakpoints.waitAndAssertNoMoreEvents();
|
||||
|
||||
await counterBreakpoints.batch(async () => {
|
||||
await setStateBreakpoint.unset();
|
||||
await setNewValBreakpoint.unset();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { expect } from 'chai';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { IChromeLaunchConfiguration } from '../../configuration';
|
||||
import { THREAD_ID } from '../testSupport/debugClient';
|
||||
|
||||
testUsing(
|
||||
'Pause on promise rejections when unhandled exceptions are enabled',
|
||||
context =>
|
||||
LaunchProject.launch(
|
||||
context,
|
||||
TestProjectSpec.fromTestPath('featuresTests/pauseOnPromisesRejections'),
|
||||
{} as IChromeLaunchConfiguration, // TODO@rob
|
||||
{
|
||||
configureDebuggee: debugClient =>
|
||||
debugClient.setExceptionBreakpointsRequest({ filters: ['uncaught'] }),
|
||||
},
|
||||
),
|
||||
async launchProject => {
|
||||
await waitUntilPausedOnPromiseRejection(launchProject, `Things didn't go as expected`);
|
||||
},
|
||||
);
|
||||
|
||||
/** Wait and block until the debuggee is paused on an unhandled promise */
|
||||
async function waitUntilPausedOnPromiseRejection(
|
||||
launchProject: LaunchProject,
|
||||
exceptionMessage: string,
|
||||
): Promise<void> {
|
||||
return launchProject.pausedWizard.waitAndConsumePausedEvent(async pauseInfo => {
|
||||
expect(pauseInfo.description).to.equal('Paused on promise rejection');
|
||||
expect(pauseInfo.reason).to.equal('exception');
|
||||
|
||||
const exceptionInfo = await launchProject.debugClient.exceptionInfoRequest({
|
||||
threadId: THREAD_ID,
|
||||
});
|
||||
validateExceptionHasCorrectInformation(exceptionInfo, exceptionMessage);
|
||||
});
|
||||
}
|
||||
|
||||
function validateExceptionHasCorrectInformation(
|
||||
exceptionInfo: DebugProtocol.ExceptionInfoResponse,
|
||||
exceptionMessage: string,
|
||||
): void {
|
||||
expect(exceptionInfo.success).to.equal(true);
|
||||
expect(exceptionInfo.body.breakMode).to.equal('unhandled');
|
||||
expect(exceptionInfo.body.description).to.equal(undefined);
|
||||
expect(exceptionInfo.body.details).to.not.equal(undefined);
|
||||
expect(exceptionInfo.body.details!.message).to.equal(exceptionMessage);
|
||||
expect(exceptionInfo.body.exceptionId).to.equal('string');
|
||||
// formattedDescription is a VS-specific property
|
||||
expect((<any>exceptionInfo.body.details).formattedDescription).to.equal(exceptionMessage);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { expect } from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import { promiseTimeout } from '../testUtils';
|
||||
|
||||
// There is no way to validate whether we are showing the paused overlay with puppeteer, so we look into the debug-adapter
|
||||
// log and see if we sent the proper Overlay.setPausedInDebuggerMessage message
|
||||
async function latestPausedOverlay(): Promise<string | undefined> {
|
||||
// Wait a little to give the log file time to get written...
|
||||
// Warning: If this test starts failing because 500 ms being too little time, we should change the logic to read the file, and retry a few times to see if the assertion passes eventually
|
||||
// If that doesn't work either, we'll need to do something less hacky like implementing a sniffer or proxy of the protocol, and get the information directly from there instead of
|
||||
// reading it from a file
|
||||
await promiseTimeout(undefined, 500);
|
||||
|
||||
// TODO@rob
|
||||
// const logFilePath = launchArgs().logFilePath!;
|
||||
// const logFileContents = await readFileP(logFilePath);
|
||||
// const lines = logFileContents.split('\n');
|
||||
// const lastEvent = _.findLast(lines, line => line.indexOf('Overlay.setPausedInDebuggerMessage') >= 0);
|
||||
// expect(lastEvent).to.not.equal(undefined);
|
||||
|
||||
// We are trying to match this string: Overlay.setPausedInDebuggerMessage\",\"params\":{ <contents here> }
|
||||
// const matches = lastEvent!.match(/Overlay\.setPausedInDebuggerMessage\\",\\"params\\":\{([^}]*)\}/);
|
||||
// expect(matches).to.not.equal(null);
|
||||
// expect(matches!.length).to.equal(2);
|
||||
// return matches![1];
|
||||
return;
|
||||
}
|
||||
|
||||
suite.skip('Pause overlay is shown', () => {
|
||||
testUsing(
|
||||
'when hitting a debugger statement',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('featuresTests/pausedOverlay')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
expect(await latestPausedOverlay()).to.equal(
|
||||
`\\"message\\":\\"Paused in Visual Studio Code\\"`,
|
||||
);
|
||||
|
||||
await launchProject.pausedWizard.resume();
|
||||
expect(await latestPausedOverlay()).to.equal(''); // An empty message removes the overlay
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { pathToFileURL } from '../testUtils';
|
||||
|
||||
const testSpec = TestProjectSpec.fromTestPath('simple');
|
||||
const appPath = testSpec.src('../index.html');
|
||||
|
||||
// appPathUrl will have on Windows a character escaped like file:///C%3A/myproject/index.html
|
||||
const appPathUrl = pathToFileURL(appPath).replace(/file:\/\/\/([a-z]):\//, 'file:///$1%3A/');
|
||||
|
||||
suite('Unusual launch.json', () => {
|
||||
testUsing(
|
||||
'Hit breakpoint when using an escape character in the url',
|
||||
context => LaunchProject.launch(context, testSpec.usingStaticUrl(appPathUrl)),
|
||||
async launchProject => {
|
||||
// Wait for the page to load
|
||||
await launchProject.page.waitForSelector('#helloWorld');
|
||||
|
||||
// Set a breakpoint, and reload to hit the breakpoint
|
||||
const breakpoint = await launchProject.breakpoints
|
||||
.at('../app.js')
|
||||
.breakpoint({ text: `console.log('Very simple webpage');` });
|
||||
await breakpoint.assertIsHitThenResumeWhen(() => launchProject.page.reload());
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,338 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { VariablesWizard } from '../wizards/variables/variablesWizard';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { testUsing } from '../fixtures/testUsing';
|
||||
|
||||
// Scopes' kinds: 'global' | 'local' | 'with' | 'closure' | 'catch' | 'block' | 'script' | 'eval' | 'module'
|
||||
// TODO: Test several scopes at the same time. They can be repeated, and the order does matter
|
||||
suite('Variables scopes', function() {
|
||||
testUsing(
|
||||
'local',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/localScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
local: `
|
||||
this = Window (Object)
|
||||
arguments = Arguments(0) [] (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function consoleDotLog(m) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'globals',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/globalScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertNewGlobalVariariablesAre(
|
||||
async () => {
|
||||
await launchProject.pausedWizard.resume();
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
},
|
||||
// The variables declared with const, and let aren't global variables so they won't appear here
|
||||
`
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function consoleDotLog(m) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = p {align: "", title: "", lang: "", …} (Object)
|
||||
evalVar1 = 16 (number)
|
||||
evalVar2 = "sdlfk" (string)
|
||||
evalVar3 = Array(3) [1, 2, 3] (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
i = 101 (number)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object) // TODO: This and other types seems wrong. Investigate
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'script',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/scriptScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
script: `
|
||||
this = Window (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'block',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/blockScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
block: `
|
||||
this = Window (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function consoleDotLog(m) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'catch',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/catchScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
catch: `
|
||||
exception = Error: Something went wrong (Object)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'closure',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/closureScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
closure: `
|
||||
arguments = Arguments(0) [] (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function consoleDotLog(m) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
pauseInside = function pauseInside() { … } (Function)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'eval',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/evalScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
eval: `
|
||||
this = Window (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a\nstring with\nnewlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'with',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/withScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
with: `
|
||||
this = Window (Object)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function (m) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
evalVar1 = 16 (number)
|
||||
evalVar2 = "sdlfk" (string)
|
||||
evalVar3 = Array(3) [1, 2, 3] (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function () { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
i = 101 (number)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a
|
||||
string with
|
||||
newlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)
|
||||
__proto__ = Object {constructor: , __defineGetter__: , __defineSetter__: , …} (Object)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
testUsing(
|
||||
'module',
|
||||
context =>
|
||||
LaunchProject.launch(context, TestProjectSpec.fromTestPath('variablesScopes/moduleScope')),
|
||||
async launchProject => {
|
||||
await launchProject.pausedWizard.waitUntilPausedOnDebuggerStatement();
|
||||
|
||||
await new VariablesWizard(launchProject.debugClient).assertTopFrameVariablesAre({
|
||||
module: `
|
||||
this = undefined (undefined)
|
||||
b = body {text: "", link: "", vLink: "", …} (Object)
|
||||
bool = true (boolean)
|
||||
buffer = ArrayBuffer(8) {} (Object)
|
||||
buffView = Int32Array(2) [234, 0] (Object)
|
||||
consoleDotLog = function consoleDotLog(m2) { … } (Function)
|
||||
e = Error: hi (Object)
|
||||
element = body {text: "", link: "", vLink: "", …} (Object)
|
||||
fn = () => { … } (Function)
|
||||
fn2 = function (param) { … } (Function)
|
||||
globalCode = "page loaded" (string)
|
||||
inf = Infinity (number)
|
||||
infStr = "Infinity" (string)
|
||||
longStr = "this is a
|
||||
string with
|
||||
newlines" (string)
|
||||
m = Map(1) {} (Object)
|
||||
manyPropsObj = Object {0: 1, 1: 3, 2: 5, …} (Object)
|
||||
myVar = Object {num: 1, str: "Global", obj: Object, …} (Object)
|
||||
nan = NaN (number)
|
||||
obj = Object {a: 2, thing: <accessor>} (Object)
|
||||
qqq = undefined (undefined)
|
||||
r = /^asdf.*$/g {lastIndex: 0} (Object)
|
||||
s = Symbol(hi) (symbol)
|
||||
str = "hello" (string)
|
||||
xyz = 4 (number)`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as testSetup from '../testSetup';
|
||||
import { IFixture } from './fixture';
|
||||
import { IBeforeAndAfterContext, ITestCallbackContext } from 'mocha';
|
||||
import { ExtendedDebugClient } from '../testSupport/debugClient';
|
||||
|
||||
/**
|
||||
* Default set up for all our tests. We expect all our tests to need to do this setup
|
||||
* which includes configure the debug adapter, logging, etc...
|
||||
*/
|
||||
export class DefaultFixture implements IFixture {
|
||||
private constructor(public readonly debugClient: ExtendedDebugClient) {
|
||||
// Running tests on CI can time out at the default 5s, so we up this to 15s
|
||||
debugClient.defaultTimeout = 15000;
|
||||
}
|
||||
|
||||
/** Create a new fixture using the provided setup context */
|
||||
public static async create(
|
||||
context: IBeforeAndAfterContext | ITestCallbackContext,
|
||||
): Promise<DefaultFixture> {
|
||||
return new DefaultFixture(await testSetup.setup(context));
|
||||
}
|
||||
|
||||
/** Create a new fixture using the full title of the test case currently running */
|
||||
public static async createWithTitle(testTitle: string): Promise<DefaultFixture> {
|
||||
return new DefaultFixture(await testSetup.setupWithTitle(testTitle));
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
// logger.log(`Default test clean-up`); // TODO@rob
|
||||
await testSetup.teardown();
|
||||
// logger.log(`Default test clean-up finished`); // TODO@rob
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `DefaultFixture`;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { PromiseOrNot } from '../testUtils';
|
||||
|
||||
/**
|
||||
* See https://en.wikipedia.org/wiki/Test_fixture for more context
|
||||
*/
|
||||
|
||||
/**
|
||||
* A fixture represents a particular piece of set up of the context, or the environment or
|
||||
* the configuration needed for a test or suite to run.
|
||||
* The fixture should make those changes during it's constructor or static constructor method,
|
||||
* and it'll "clean up" those changes with the cleanUp method
|
||||
*/
|
||||
export interface IFixture {
|
||||
/** Clean-up the context, or changes made by the fixture */
|
||||
cleanUp(): PromiseOrNot<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A fixture representing that no setup is needed
|
||||
*/
|
||||
export class NullFixture implements IFixture {
|
||||
public cleanUp(): void {}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { IFixture } from './fixture';
|
||||
import { DefaultFixture } from './defaultFixture';
|
||||
import { LaunchWebServer, ProvideStaticUrl } from './launchWebServer';
|
||||
import { Page, Browser } from 'puppeteer';
|
||||
import { ITestCallbackContext, IBeforeAndAfterContext } from 'mocha';
|
||||
import { URL } from 'url';
|
||||
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../../configuration';
|
||||
import { ExtendedDebugClient } from '../testSupport/debugClient';
|
||||
import { PausedWizard } from '../wizards/pausedWizard';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
import { LaunchPuppeteer } from '../puppeteer/launchPuppeteer';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { IDebugAdapterCallbacks, IScenarioConfiguration } from '../intTestSupport';
|
||||
|
||||
/** Perform all the steps neccesary to launch a particular project such as:
|
||||
* - Default fixture/setup
|
||||
* - Launch web-server
|
||||
* - Connect puppeteer to Chrome
|
||||
*/
|
||||
export class LaunchProject implements IFixture {
|
||||
private constructor(
|
||||
private readonly _defaultFixture: DefaultFixture,
|
||||
private readonly _launchWebServer: LaunchWebServer | ProvideStaticUrl,
|
||||
public readonly pausedWizard: PausedWizard,
|
||||
public readonly breakpoints: BreakpointsWizard,
|
||||
private readonly _launchPuppeteer: LaunchPuppeteer,
|
||||
) {}
|
||||
|
||||
public static async launch(
|
||||
testContext: IBeforeAndAfterContext | ITestCallbackContext,
|
||||
testSpec: TestProjectSpec,
|
||||
launchConfig: IChromeLaunchConfiguration = {} as IChromeLaunchConfiguration,
|
||||
callbacks: IDebugAdapterCallbacks = {},
|
||||
): Promise<LaunchProject> {
|
||||
return this.start(testContext, testSpec, { ...launchConfig, scenario: 'launch' }, callbacks);
|
||||
}
|
||||
|
||||
public static async attach(
|
||||
testContext: IBeforeAndAfterContext | ITestCallbackContext,
|
||||
testSpec: TestProjectSpec,
|
||||
attachConfig: IChromeAttachConfiguration = { port: 0 } as IChromeAttachConfiguration,
|
||||
callbacks: IDebugAdapterCallbacks = {},
|
||||
): Promise<LaunchProject> {
|
||||
return this.start(testContext, testSpec, { ...attachConfig, scenario: 'attach' }, callbacks);
|
||||
}
|
||||
|
||||
public static async start(
|
||||
testContext: IBeforeAndAfterContext | ITestCallbackContext,
|
||||
testSpec: TestProjectSpec,
|
||||
daConfig: IScenarioConfiguration,
|
||||
callbacks: IDebugAdapterCallbacks,
|
||||
): Promise<LaunchProject> {
|
||||
const launchWebServer = testSpec.staticUrl
|
||||
? new ProvideStaticUrl(new URL(testSpec.staticUrl), testSpec)
|
||||
: await LaunchWebServer.launch(testSpec);
|
||||
|
||||
const defaultFixture = await DefaultFixture.create(testContext);
|
||||
|
||||
// We need to create the PausedWizard before launching the debuggee to listen to all events and avoid race conditions
|
||||
const pausedWizard = PausedWizard.forClient(defaultFixture.debugClient);
|
||||
const breakpointsWizard = BreakpointsWizard.createWithPausedWizard(
|
||||
defaultFixture.debugClient,
|
||||
pausedWizard,
|
||||
testSpec,
|
||||
);
|
||||
|
||||
const chromeArgsForPuppeteer =
|
||||
daConfig.scenario === 'attach' ? [launchWebServer.url.toString()] : []; // For attach we need to launch puppeteer/chrome pointing to the web-server
|
||||
const launchConfig = { ...launchWebServer.launchConfig };
|
||||
|
||||
const launchPuppeteer = await LaunchPuppeteer.start(
|
||||
defaultFixture.debugClient,
|
||||
{ ...launchConfig, ...daConfig },
|
||||
chromeArgsForPuppeteer,
|
||||
callbacks,
|
||||
);
|
||||
return new LaunchProject(
|
||||
defaultFixture,
|
||||
launchWebServer,
|
||||
pausedWizard,
|
||||
breakpointsWizard,
|
||||
launchPuppeteer,
|
||||
);
|
||||
}
|
||||
|
||||
/** Client for the debug adapter being used for this test */
|
||||
public get debugClient(): ExtendedDebugClient {
|
||||
return this._defaultFixture.debugClient;
|
||||
}
|
||||
|
||||
/** Object to control the debugged browser via puppeteer */
|
||||
public get browser(): Browser {
|
||||
return this._launchPuppeteer.browser;
|
||||
}
|
||||
|
||||
/** Object to control the debugged page via puppeteer */
|
||||
public get page(): Page {
|
||||
return this._launchPuppeteer.page;
|
||||
}
|
||||
|
||||
public get url(): URL {
|
||||
return this._launchWebServer.url;
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
await this.pausedWizard.waitAndAssertNoMoreEvents();
|
||||
await this._defaultFixture.cleanUp(); // Disconnect the debug-adapter first
|
||||
await this._launchPuppeteer.cleanUp(); // Then disconnect puppeteer and close chrome
|
||||
await this._launchWebServer.cleanUp(); // Finally disconnect the web-server
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { createServer } from 'http-server';
|
||||
import { IFixture } from './fixture';
|
||||
import { URL } from 'url';
|
||||
import { HttpOrHttpsServer } from '../types/server';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
import { IChromeLaunchConfiguration } from '../../configuration';
|
||||
import { AddressInfo } from 'net';
|
||||
|
||||
async function createServerAsync(root: string): Promise<HttpOrHttpsServer> {
|
||||
const server = createServer({ root });
|
||||
return await new Promise((resolve, reject) => {
|
||||
// logger.log(`About to launch web-server on: ${root}`);
|
||||
server.listen(0, '127.0.0.1', function(this: HttpOrHttpsServer, error?: any) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
// logger.log(`Web-server on: ${root} listening on: ${JSON.stringify(this.address())}`);
|
||||
resolve(this); // We return the this pointer which is the internal server object, which has access to the .address() method
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function closeServer(server: HttpOrHttpsServer): Promise<void> {
|
||||
// logger.log(`Closing web-server`);
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error?: any) => {
|
||||
if (error) {
|
||||
// logger.log('Error closing server in teardown: ' + (error && error.message));
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// logger.log(`Web-server closed`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a web-server for the test project listening on the default port
|
||||
*/
|
||||
export class LaunchWebServer implements IFixture {
|
||||
private constructor(
|
||||
private readonly _server: HttpOrHttpsServer,
|
||||
public readonly testSpec: TestProjectSpec,
|
||||
) {}
|
||||
|
||||
public static async launch(testSpec: TestProjectSpec): Promise<LaunchWebServer> {
|
||||
return new LaunchWebServer(await createServerAsync(testSpec.props.webRoot), testSpec);
|
||||
}
|
||||
|
||||
public get url(): URL {
|
||||
return new URL(`http://localhost:${this.port}/`);
|
||||
}
|
||||
|
||||
public get launchConfig(): IChromeLaunchConfiguration {
|
||||
return Object.assign({}, this.testSpec.props.launchConfig, {
|
||||
url: this.url.toString(),
|
||||
}) as IChromeLaunchConfiguration; // TODO@rob
|
||||
}
|
||||
|
||||
public get port(): number {
|
||||
const address = this._server.address();
|
||||
return (address as AddressInfo).port;
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
await closeServer(this._server);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `LaunchWebServer`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProvideStaticUrl implements IFixture {
|
||||
public constructor(public readonly url: URL, public readonly testSpec: TestProjectSpec) {}
|
||||
|
||||
public get launchConfig(): IChromeLaunchConfiguration {
|
||||
return {
|
||||
...this.testSpec.props.launchConfig,
|
||||
url: this.url.href,
|
||||
} as IChromeLaunchConfiguration; // TODO@rob
|
||||
}
|
||||
cleanUp() {}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { IFixture } from './fixture';
|
||||
import { asyncMap } from '../core-v2/chrome/collections/async';
|
||||
|
||||
/** Combine multiple fixtures into a single fixture, for easier management (e.g. you just need to call a single cleanUp method) */
|
||||
export class MultipleFixtures implements IFixture {
|
||||
private readonly _fixtures: IFixture[];
|
||||
|
||||
public constructor(...fixtures: IFixture[]) {
|
||||
this._fixtures = fixtures;
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
await asyncMap(this._fixtures, fixture => fixture.cleanUp());
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { IFixture } from './fixture';
|
||||
import { ITestCallbackContext } from 'mocha';
|
||||
import { PromiseOrNot } from '../testUtils';
|
||||
|
||||
/** Run a test doing the setup/cleanup indicated by the provided fixtures */
|
||||
function testUsingFunction<T extends IFixture>(
|
||||
expectation: string,
|
||||
fixtureProvider: (context: ITestCallbackContext) => PromiseOrNot<T>,
|
||||
testFunction: (fixtures: T) => Promise<void>,
|
||||
): void {
|
||||
suite(expectation, function() {
|
||||
let fixture: T | undefined;
|
||||
test(expectation, async function() {
|
||||
fixture = await fixtureProvider(this);
|
||||
await testFunction(fixture);
|
||||
});
|
||||
teardown(() => {
|
||||
if (fixture) {
|
||||
return fixture.cleanUp();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
testUsingFunction.skip = <T extends IFixture>(
|
||||
expectation: string,
|
||||
_fixtureProvider: (context: ITestCallbackContext) => PromiseOrNot<T>,
|
||||
_testFunction: (fixtures: T) => Promise<void>,
|
||||
) =>
|
||||
test.skip(expectation, () => {
|
||||
throw new Error(`We don't expect this to be called`);
|
||||
});
|
||||
|
||||
export const testUsing = testUsingFunction;
|
|
@ -1 +0,0 @@
|
|||
This folder contains tests that apply to specific frameworks or project types (e.g. React, Angular, Vue, etc.)
|
|
@ -1,76 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Integration tests for the React framework
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as testSetup from '../testSetup';
|
||||
import { setBreakpoint, setConditionalBreakpoint } from '../intTestSupport';
|
||||
import { FrameworkTestSuite } from './frameworkCommonTests';
|
||||
import { TestProjectSpec } from './frameworkTestSupport';
|
||||
import { puppeteerSuite, puppeteerTest } from '../puppeteer/puppeteerSuite';
|
||||
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
const REACT_PROJECT_ROOT = path.join(DATA_ROOT, 'react', 'dist');
|
||||
const TEST_SPEC = new TestProjectSpec({ projectRoot: REACT_PROJECT_ROOT });
|
||||
|
||||
// This test doesn't use puppeteer, so we leave it outside the suite
|
||||
// testBreakOnLoad('React', TEST_SPEC, 'react_App_render'); // TODO@rob break on load
|
||||
|
||||
puppeteerSuite.skip('React Framework Tests', TEST_SPEC, suiteContext => {
|
||||
// Need Chrome attach mode
|
||||
|
||||
suite('Common Framework Tests', () => {
|
||||
const frameworkTests = new FrameworkTestSuite('React', suiteContext);
|
||||
frameworkTests.testPageReloadBreakpoint('react_App_render');
|
||||
frameworkTests.testPauseExecution();
|
||||
frameworkTests.testStepOver('react_Counter_increment');
|
||||
frameworkTests.testStepOut('react_Counter_increment', 'react_Counter_stepOut');
|
||||
frameworkTests.testStepIn('react_Counter_stepInStop', 'react_Counter_stepIn');
|
||||
});
|
||||
|
||||
suite('React specific tests', () => {
|
||||
puppeteerTest('Should hit breakpoint in .jsx file', suiteContext, async (_context, page) => {
|
||||
const pausedWizard = suiteContext.launchProject!.pausedWizard;
|
||||
|
||||
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
await setBreakpoint(suiteContext.debugClient, location);
|
||||
const clicked = incBtn.click();
|
||||
await suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
|
||||
await pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
await pausedWizard.resume();
|
||||
await clicked;
|
||||
});
|
||||
|
||||
puppeteerTest(
|
||||
'Should hit conditional breakpoint in .jsx file',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const pausedWizard = suiteContext.launchProject!.pausedWizard;
|
||||
|
||||
const location = suiteContext.breakpointLabels.get('react_Counter_increment');
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
|
||||
await setConditionalBreakpoint(suiteContext.debugClient, location, 'this.state.count == 2');
|
||||
// click 3 times, state will be = 2 on the third click
|
||||
await incBtn.click();
|
||||
await incBtn.click();
|
||||
// don't await the last click, as the stopped debugger will deadlock it
|
||||
const clicked = incBtn.click();
|
||||
await suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
|
||||
await pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
// Be sure to await the continue request, otherwise sometimes the last click promise will
|
||||
// be rejected because the chrome instance is closed before it completes.
|
||||
await pausedWizard.resume();
|
||||
await clicked;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { loadProjectLabels } from '../labels';
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
|
||||
suite('Test framework tests', () => {
|
||||
test('Should correctly find breakpoint labels in test source files', async () => {
|
||||
const labels = await loadProjectLabels('./testdata');
|
||||
const worldLabel = labels.get('WorldLabel');
|
||||
|
||||
expect(worldLabel.path).to.eql(path.join('testdata', 'labelTest.ts'));
|
||||
expect(worldLabel.line).to.eql(9);
|
||||
});
|
||||
|
||||
test('Should correctly find block comment breakpoint labels in test source files', async () => {
|
||||
const labels = await loadProjectLabels('./testdata');
|
||||
const blockLabel = labels.get('blockLabel');
|
||||
|
||||
expect(blockLabel.path).to.eql(path.join('testdata', 'labelTest.ts'));
|
||||
expect(blockLabel.line).to.eql(10);
|
||||
});
|
||||
});
|
|
@ -1,235 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { setBreakpoint } from '../intTestSupport';
|
||||
import { LaunchWebServer } from '../fixtures/launchWebServer';
|
||||
import { loadProjectLabels } from '../labels';
|
||||
import { TestProjectSpec } from './frameworkTestSupport';
|
||||
import { DefaultFixture } from '../fixtures/defaultFixture';
|
||||
import { MultipleFixtures } from '../fixtures/multipleFixtures';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { BreakpointsWizard } from '../wizards/breakpoints/breakpointsWizard';
|
||||
import { PuppeteerTestContext, puppeteerTest } from '../puppeteer/puppeteerSuite';
|
||||
import { PausedWizard } from '../wizards/pausedWizard';
|
||||
|
||||
/**
|
||||
* A common framework test suite that allows for easy (one-liner) testing of various
|
||||
* functionality in different framework projects (note: this isn't a suite in the mocha sense, but rather
|
||||
* a collection of functions that return mocha tests)
|
||||
*/
|
||||
export class FrameworkTestSuite {
|
||||
constructor(private frameworkName: string, private suiteContext: PuppeteerTestContext) {}
|
||||
|
||||
private get pausedWizard(): PausedWizard {
|
||||
return this.suiteContext.launchProject!.pausedWizard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a breakpoint set after the page loads is hit on reload
|
||||
* @param bpLabel Label for the breakpoint to set
|
||||
*/
|
||||
testPageReloadBreakpoint(bpLabel: string) {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - Should hit breakpoint on page reload`,
|
||||
this.suiteContext,
|
||||
async (context, page) => {
|
||||
const debugClient = context.debugClient;
|
||||
const bpLocation = context.breakpointLabels.get(bpLabel);
|
||||
|
||||
// wait for something on the page to ensure we're fully loaded, TODO: make this more generic?
|
||||
await page.waitForSelector('#incrementBtn');
|
||||
|
||||
await setBreakpoint(debugClient, bpLocation);
|
||||
const reloaded = page.reload();
|
||||
|
||||
await debugClient.assertStoppedLocation('breakpoint', bpLocation);
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
await debugClient.continueRequest();
|
||||
await this.pausedWizard.waitAndConsumeResumedEvent();
|
||||
|
||||
await reloaded;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that step in command works as expected.
|
||||
* @param bpLabelStop Label for the breakpoint to set
|
||||
* @param bpLabelStepIn Label for the location where the 'step out' command should land us
|
||||
*/
|
||||
testStepIn(bpLabelStop: string, bpLabelStepIn: string) {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - Should step in correctly`,
|
||||
this.suiteContext,
|
||||
async (_context, page) => {
|
||||
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
|
||||
const stepInLocation = this.suiteContext.breakpointLabels.get(bpLabelStepIn);
|
||||
|
||||
// wait for selector **before** setting breakpoint to avoid race conditions against scriptParsed event
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
await setBreakpoint(this.suiteContext.debugClient, location);
|
||||
const clicked = incBtn.click();
|
||||
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation(
|
||||
'step',
|
||||
stepInLocation,
|
||||
);
|
||||
await this.suiteContext.debugClient.stepInAndStop();
|
||||
await this.pausedWizard.waitAndConsumeResumedEvent();
|
||||
|
||||
await stopOnStep;
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
await this.pausedWizard.resume();
|
||||
await clicked;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that step over (next) command works as expected.
|
||||
* Note: currently this test assumes that next will land us on the very next line in the file.
|
||||
* @param bpLabel Label for the breakpoint to set
|
||||
*/
|
||||
testStepOver(bpLabel: string) {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - Should step over correctly`,
|
||||
this.suiteContext,
|
||||
async (_context, page) => {
|
||||
const location = this.suiteContext.breakpointLabels.get(bpLabel);
|
||||
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
await setBreakpoint(this.suiteContext.debugClient, location);
|
||||
const clicked = incBtn.click();
|
||||
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation('step', {
|
||||
...location,
|
||||
line: location.line + 1,
|
||||
});
|
||||
await this.suiteContext.debugClient.nextAndStop();
|
||||
await this.pausedWizard.waitAndConsumeResumedEvent();
|
||||
|
||||
await stopOnStep;
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
await this.pausedWizard.resume();
|
||||
await clicked;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that step out command works as expected.
|
||||
* @param bpLabelStop Label for the breakpoint to set
|
||||
* @param bpLabelStepOut Label for the location where the 'step out' command should land us
|
||||
*/
|
||||
testStepOut(bpLabelStop: string, bpLabelStepOut: string) {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - Should step out correctly`,
|
||||
this.suiteContext,
|
||||
async (_context, page) => {
|
||||
const location = this.suiteContext.breakpointLabels.get(bpLabelStop);
|
||||
const stepOutLocation = this.suiteContext.breakpointLabels.get(bpLabelStepOut);
|
||||
|
||||
const incBtn = await page.waitForSelector('#incrementBtn');
|
||||
await setBreakpoint(this.suiteContext.debugClient, location);
|
||||
const clicked = incBtn.click();
|
||||
await this.suiteContext.debugClient.assertStoppedLocation('breakpoint', location);
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
const stopOnStep = this.suiteContext.debugClient.assertStoppedLocation(
|
||||
'step',
|
||||
stepOutLocation,
|
||||
);
|
||||
await this.suiteContext.debugClient.stepOutAndStop();
|
||||
await this.pausedWizard.waitAndConsumeResumedEvent();
|
||||
|
||||
await stopOnStep;
|
||||
await this.pausedWizard.waitAndConsumePausedEvent(() => {});
|
||||
|
||||
await this.pausedWizard.resume();
|
||||
await clicked;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the debug adapter can correctly pause execution
|
||||
* @param bpLocation
|
||||
*/
|
||||
testPauseExecution() {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - Should correctly pause execution on a pause request`,
|
||||
this.suiteContext,
|
||||
async (_context, _page) => {
|
||||
await this.pausedWizard.pause();
|
||||
|
||||
// TODO: Verify we are actually pausing in the expected line
|
||||
|
||||
await this.pausedWizard.resume();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic breakpoint test. This can be used for many different types of breakpoint tests with the following structure:
|
||||
*
|
||||
* 1. Wait for the page to load by waiting for the selector: `waitSelectorId`
|
||||
* 2. Set a breakpoint at `bpLabel`
|
||||
* 3. Execute a trigger event that should cause the breakpoint to be hit using the function `trigger`
|
||||
* 4. Assert that the breakpoint is hit on the expected location, and continue
|
||||
*
|
||||
* @param waitSelector an html selector to identify a resource to wait for for page load
|
||||
* @param bpLabel
|
||||
* @param trigger
|
||||
*/
|
||||
testBreakpointHitsOnPageAction(
|
||||
description: string,
|
||||
waitSelector: string,
|
||||
file: string,
|
||||
bpLabel: string,
|
||||
trigger: (page: puppeteer.Page) => Promise<void>,
|
||||
) {
|
||||
return puppeteerTest(
|
||||
`${this.frameworkName} - ${description}`,
|
||||
this.suiteContext,
|
||||
async (context, page) => {
|
||||
await page.waitForSelector(`${waitSelector}`);
|
||||
const breakpoints = BreakpointsWizard.create(context.debugClient, context.testSpec);
|
||||
const breakpointWizard = breakpoints.at(file);
|
||||
const bp = await breakpointWizard.breakpoint({ text: bpLabel });
|
||||
await bp.assertIsHitThenResumeWhen(() => trigger(page));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that we can stop on a breakpoint set before launch
|
||||
* @param bpLabel Label for the breakpoint to set
|
||||
*/
|
||||
export function testBreakOnLoad(frameworkName: string, testSpec: TestProjectSpec, bpLabel: string) {
|
||||
const testTitle = `${frameworkName} - Should stop on breakpoint on initial page load`;
|
||||
return test(testTitle, async () => {
|
||||
const defaultFixture = await DefaultFixture.createWithTitle(testTitle);
|
||||
const launchWebServer = await LaunchWebServer.launch(testSpec);
|
||||
const fixture = new MultipleFixtures(launchWebServer, defaultFixture);
|
||||
|
||||
try {
|
||||
const breakpointLabels = await loadProjectLabels(testSpec.props.webRoot);
|
||||
const location = breakpointLabels.get(bpLabel);
|
||||
await defaultFixture.debugClient.hitBreakpointUnverified(
|
||||
launchWebServer.launchConfig,
|
||||
location,
|
||||
);
|
||||
} finally {
|
||||
await fixture.cleanUp();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
const puppeteer = require('puppeteer');
|
||||
import { BreakpointLocation } from '../intTestSupport';
|
||||
import { IValidatedMap } from '../core-v2/chrome/collections/validatedMap';
|
||||
import { DATA_ROOT } from '../testSetup';
|
||||
import { IChromeTestLaunchConfiguration } from '../testUtils';
|
||||
import { ExtendedDebugClient } from '../testSupport/debugClient';
|
||||
|
||||
/*
|
||||
* A collection of supporting classes/functions for running framework tests
|
||||
*/
|
||||
|
||||
export interface ProjectSpecProps {
|
||||
/** The root directory of the test project */
|
||||
projectRoot: string;
|
||||
/** Source files directory of the test project */
|
||||
projectSrc?: string;
|
||||
/** The directory from which to host the project for a test */
|
||||
webRoot?: string;
|
||||
/** The outfiles directory for the test project */
|
||||
outFiles?: string[];
|
||||
/** The default launch configuration for the test project */
|
||||
launchConfig?: Partial<IChromeTestLaunchConfiguration>; // TODO@rob
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies an integration test project (i.e. a project that will be launched and
|
||||
* attached to in order to test the debug adapter)
|
||||
*/
|
||||
export class TestProjectSpec {
|
||||
_props: Required<ProjectSpecProps>;
|
||||
get props() {
|
||||
return this._props;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param props Parameters for the project spec. The only required param is "projectRoot", others will be set to sensible defaults
|
||||
*/
|
||||
constructor(props: ProjectSpecProps, public readonly staticUrl?: string) {
|
||||
const outFiles = props.outFiles || [path.join(props.projectRoot, 'out')];
|
||||
const webRoot = props.webRoot || props.projectRoot;
|
||||
this._props = {
|
||||
projectRoot: props.projectRoot,
|
||||
projectSrc: props.projectSrc || path.join(props.projectRoot, 'src'),
|
||||
webRoot: webRoot,
|
||||
outFiles: outFiles,
|
||||
launchConfig: props.launchConfig || {
|
||||
// outFiles: outFiles, // TODO@rob
|
||||
sourceMaps: true,
|
||||
runtimeExecutable: puppeteer.executablePath(),
|
||||
webRoot: webRoot,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify project by it's location relative to the testdata folder e.g.:
|
||||
* - TestProjectSpec.fromTestPath('react_with_loop/dist')
|
||||
* - TestProjectSpec.fromTestPath('simple')
|
||||
*
|
||||
* The path *can only* use forward-slahes "/" as separators
|
||||
*/
|
||||
public static fromTestPath(
|
||||
reversedSlashesRelativePath: string,
|
||||
sourceDir = 'src',
|
||||
staticUrl?: string,
|
||||
): TestProjectSpec {
|
||||
const pathComponents = reversedSlashesRelativePath.split('/');
|
||||
const projectAbsolutePath = path.join(...[DATA_ROOT].concat(pathComponents));
|
||||
const projectSrc = path.join(projectAbsolutePath, sourceDir);
|
||||
const props: ProjectSpecProps = { projectRoot: projectAbsolutePath, projectSrc };
|
||||
return new TestProjectSpec(props, staticUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path to a source file
|
||||
* @param filename
|
||||
*/
|
||||
src(filename: string) {
|
||||
return path.join(this.props.projectSrc, filename);
|
||||
}
|
||||
|
||||
public usingStaticUrl(staticUrl: string): TestProjectSpec {
|
||||
return new TestProjectSpec(this.props, staticUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for all the relevant context info needed to run a debug adapter test
|
||||
*/
|
||||
export interface FrameworkTestContext {
|
||||
/** The test project specs for the currently executing test suite */
|
||||
readonly testSpec: TestProjectSpec;
|
||||
/** A mapping of labels set in source files to a breakpoint location for a test */
|
||||
readonly breakpointLabels: IValidatedMap<string, BreakpointLocation>;
|
||||
/** The debug adapter test support client */
|
||||
readonly debugClient: ExtendedDebugClient;
|
||||
}
|
||||
|
||||
export class ReassignableFrameworkTestContext implements FrameworkTestContext {
|
||||
private _wrapped: FrameworkTestContext = new NotInitializedFrameworkTestContext();
|
||||
|
||||
public get testSpec(): TestProjectSpec {
|
||||
return this._wrapped.testSpec;
|
||||
}
|
||||
|
||||
public get breakpointLabels(): IValidatedMap<string, BreakpointLocation> {
|
||||
return this._wrapped.breakpointLabels;
|
||||
}
|
||||
|
||||
public get debugClient(): ExtendedDebugClient {
|
||||
return this._wrapped.debugClient;
|
||||
}
|
||||
|
||||
public reassignTo(newWrapped: FrameworkTestContext): this {
|
||||
this._wrapped = newWrapped;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class NotInitializedFrameworkTestContext implements FrameworkTestContext {
|
||||
public get testSpec(): TestProjectSpec {
|
||||
return this.throwNotInitializedException();
|
||||
}
|
||||
|
||||
public get breakpointLabels(): IValidatedMap<string, BreakpointLocation> {
|
||||
return this.throwNotInitializedException();
|
||||
}
|
||||
|
||||
public get debugClient(): ExtendedDebugClient {
|
||||
return this.throwNotInitializedException();
|
||||
}
|
||||
|
||||
private throwNotInitializedException(): never {
|
||||
throw new Error(
|
||||
`This test context hasn't been initialized yet. This is probably a bug in the tests`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* This file contains support functions to make integration testing easier
|
||||
*/
|
||||
|
||||
import { DebugClient } from 'vscode-debugadapter-testsupport';
|
||||
import { PromiseOrNot } from './testUtils';
|
||||
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../configuration';
|
||||
|
||||
const ImplementsBreakpointLocation = Symbol();
|
||||
/**
|
||||
* Simple breakpoint location params (based on what the debug test client accepts)
|
||||
*/
|
||||
export class BreakpointLocation {
|
||||
[ImplementsBreakpointLocation]: 'BreakpointLocation';
|
||||
|
||||
public constructor(
|
||||
/** The path to the source file in which to set a breakpoint */
|
||||
public readonly path: string,
|
||||
/** The line number in the file to set a breakpoint on */
|
||||
public readonly line: number,
|
||||
/** Optional breakpoint column */
|
||||
public readonly column?: number,
|
||||
/** Whether or not we should assert if the bp is verified or not */
|
||||
public readonly verified?: boolean,
|
||||
) {}
|
||||
|
||||
public toString(): string {
|
||||
return `${this.path}:${this.line}:${this.column} verified: ${this.verified}`;
|
||||
}
|
||||
}
|
||||
|
||||
export type IScenarioConfiguration =
|
||||
| (IChromeLaunchConfiguration & { scenario: 'launch' })
|
||||
| (IChromeAttachConfiguration & { scenario: 'attach' });
|
||||
|
||||
export interface IDebugAdapterCallbacks {
|
||||
registerListeners?: (client: DebugClient) => PromiseOrNot<unknown>;
|
||||
configureDebuggee?: (client: DebugClient) => PromiseOrNot<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch an instance of chrome and wait for the debug adapter to initialize and attach
|
||||
* @param client Debug Client
|
||||
* @param launchConfig The launch config to use
|
||||
*/
|
||||
export async function launchTestAdapter(
|
||||
client: DebugClient,
|
||||
launchConfig: IScenarioConfiguration,
|
||||
callbacks: IDebugAdapterCallbacks,
|
||||
) {
|
||||
const init = client.waitForEvent('initialized');
|
||||
|
||||
if (callbacks.registerListeners !== undefined) {
|
||||
await callbacks.registerListeners(client);
|
||||
}
|
||||
|
||||
if (launchConfig.scenario === 'attach') {
|
||||
delete launchConfig.url; // We don't want the url property when we attach
|
||||
|
||||
await client.initializeRequest();
|
||||
await client.attachRequest(launchConfig);
|
||||
} else {
|
||||
await client.launch(launchConfig);
|
||||
}
|
||||
|
||||
await init;
|
||||
if (callbacks.configureDebuggee !== undefined) {
|
||||
await callbacks.configureDebuggee(client);
|
||||
}
|
||||
await client.configurationDoneRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Easier way to set breakpoints for testing
|
||||
* @param client DebugClient
|
||||
* @param location Breakpoint location
|
||||
*/
|
||||
export function setBreakpoint(
|
||||
client: DebugClient,
|
||||
location: { path: string; line: number; column?: number; verified?: boolean },
|
||||
) {
|
||||
return client.setBreakpointsRequest({
|
||||
lines: [location.line],
|
||||
breakpoints: [{ line: location.line, column: location.column }],
|
||||
source: { path: location.path },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a conditional breakpoint in a file
|
||||
* @param client DebugClient
|
||||
* @param location Desired breakpoint location
|
||||
* @param condition The condition on which the breakpoint should be hit
|
||||
*/
|
||||
export function setConditionalBreakpoint(
|
||||
client: DebugClient,
|
||||
location: { path: string; line: number; column?: number; verified?: boolean },
|
||||
condition: string,
|
||||
) {
|
||||
return client.setBreakpointsRequest({
|
||||
lines: [location.line],
|
||||
breakpoints: [{ line: location.line, column: location.column, condition }],
|
||||
source: { path: location.path },
|
||||
});
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { BreakpointLocation } from './intTestSupport';
|
||||
import * as fs from 'fs';
|
||||
import * as util from 'util';
|
||||
import * as readline from 'readline';
|
||||
import * as path from 'path';
|
||||
import { ValidatedMap, IValidatedMap } from './core-v2/chrome/collections/validatedMap';
|
||||
|
||||
/*
|
||||
* Contains classes and functions to find and use test breakpoint labels in test project files
|
||||
*/
|
||||
|
||||
const readdirAsync = util.promisify(fs.readdir);
|
||||
|
||||
const labelRegex = /(\/\/|\/\*)\s*bpLabel:\s*(.+?)\b/;
|
||||
const ignoreList = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
path.join('dist', 'out'),
|
||||
path.join('testdata', 'react', 'src'),
|
||||
];
|
||||
|
||||
/**
|
||||
* A label in a source file that tells us where to put a breakpoint for a specific test
|
||||
*/
|
||||
export interface BreakpointLabel {
|
||||
label: string;
|
||||
location: BreakpointLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all breakpoint labels that exist in the 'projectRoot' directory
|
||||
* @param projectRoot Root directory for the test project
|
||||
*/
|
||||
export async function loadProjectLabels(
|
||||
projectRoot: string,
|
||||
): Promise<IValidatedMap<string, BreakpointLocation>> {
|
||||
const labelMap = new ValidatedMap<string, BreakpointLocation>();
|
||||
if (containsIgnores(projectRoot)) return labelMap;
|
||||
|
||||
const files = await readdirAsync(projectRoot);
|
||||
|
||||
for (const file of files) {
|
||||
let subMap: Map<string, BreakpointLocation> | null = null;
|
||||
const fullPath = path.join(projectRoot, file);
|
||||
if (fs.lstatSync(fullPath).isDirectory()) {
|
||||
subMap = await loadProjectLabels(fullPath);
|
||||
} else {
|
||||
subMap = await loadLabelsFromFile(fullPath);
|
||||
}
|
||||
|
||||
for (const entry of subMap.entries()) {
|
||||
labelMap.set(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return labelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load breakpoint labels from a specific file
|
||||
* @param filePath
|
||||
*/
|
||||
export async function loadLabelsFromFile(
|
||||
filePath: string,
|
||||
): Promise<Map<string, BreakpointLocation>> {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const labelMap = new Map<string, BreakpointLocation>();
|
||||
let lineNumber = 1; // breakpoint locations start at 1
|
||||
|
||||
const lineReader = readline.createInterface({
|
||||
input: fileStream,
|
||||
});
|
||||
|
||||
lineReader.on('line', fileLine => {
|
||||
const match = labelRegex.exec(fileLine);
|
||||
|
||||
if (match) {
|
||||
labelMap.set(match[2], new BreakpointLocation(filePath, lineNumber));
|
||||
}
|
||||
lineNumber++;
|
||||
});
|
||||
|
||||
const waitForClose = new Promise((accept, _reject) => {
|
||||
lineReader.on('close', () => {
|
||||
accept();
|
||||
});
|
||||
});
|
||||
|
||||
await waitForClose;
|
||||
return labelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our filepath contains anything from our ignore list
|
||||
* @param filePath
|
||||
*/
|
||||
function containsIgnores(filePath: string) {
|
||||
return ignoreList.find(ignoreItem => filePath.includes(ignoreItem));
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
Functions and test suite extensions to run integration tests with puppeteer on Chrome.
|
|
@ -1,85 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import getPort from 'get-port';
|
||||
import { IFixture } from '../fixtures/fixture';
|
||||
import {
|
||||
launchTestAdapter,
|
||||
IScenarioConfiguration,
|
||||
IDebugAdapterCallbacks,
|
||||
} from '../intTestSupport';
|
||||
import { connectPuppeteer, getPageByUrl, launchPuppeteer } from './puppeteerSupport';
|
||||
import { logCallsTo } from '../utils/logging';
|
||||
import { isThisV1 } from '../testSetup';
|
||||
import { Browser, Page } from 'puppeteer';
|
||||
import { ExtendedDebugClient } from '../testSupport/debugClient';
|
||||
import * as utils from '../testUtils';
|
||||
|
||||
/**
|
||||
* Launch the debug adapter using the Puppeteer version of chrome, and then connect to it
|
||||
*
|
||||
* The fixture offers access to both the browser, and page objects of puppeteer
|
||||
*/
|
||||
export class LaunchPuppeteer implements IFixture {
|
||||
public constructor(public readonly browser: Browser, public readonly page: Page) {}
|
||||
|
||||
public static async start(
|
||||
debugClient: ExtendedDebugClient,
|
||||
daConfig: IScenarioConfiguration,
|
||||
chromeArgs: string[] = [],
|
||||
callbacks: IDebugAdapterCallbacks,
|
||||
): Promise<LaunchPuppeteer> {
|
||||
const daPort = await getPort();
|
||||
// logger.log(`About to ${daConfig.scenario} debug-adapter at port: ${daPort}`); // TODO@rob
|
||||
|
||||
let browser: Browser;
|
||||
if (daConfig.scenario === 'launch') {
|
||||
await launchTestAdapter(
|
||||
debugClient,
|
||||
Object.assign({}, daConfig, { runtimeArgs: [`--remote-debugging-port=${daPort}`] }),
|
||||
callbacks,
|
||||
);
|
||||
browser = await connectPuppeteer(daPort);
|
||||
} else {
|
||||
browser = await launchPuppeteer(daPort, chromeArgs);
|
||||
|
||||
// We want to attach after the page is fully loaded, and things happened, to simulate a real attach scenario. So we wait for a little bit
|
||||
await utils.promiseTimeout(undefined, 1000);
|
||||
|
||||
await launchTestAdapter(
|
||||
debugClient,
|
||||
Object.assign({}, daConfig, { port: daPort }),
|
||||
callbacks,
|
||||
);
|
||||
}
|
||||
|
||||
const page = logCallsTo(await getPageByUrl(browser, daConfig.url!), 'PuppeteerPage');
|
||||
|
||||
// This short wait appears to be necessary to completely avoid a race condition in V1 (tried several other
|
||||
// strategies to wait deterministically for all scripts to be loaded and parsed, but have been unsuccessful so far)
|
||||
// If we don't wait here, there's always a possibility that we can send the set breakpoint request
|
||||
// for a subsequent test after the scripts have started being parsed/run by Chrome, yet before
|
||||
// the target script is parsed, in which case the adapter will try to use breakOnLoad, but
|
||||
// the instrumentation BP will never be hit, leaving our breakpoint in limbo
|
||||
if (isThisV1) {
|
||||
await new Promise(a => setTimeout(a, 500));
|
||||
}
|
||||
|
||||
return new LaunchPuppeteer(browser, page);
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
// logger.log(`Closing puppeteer and chrome`);
|
||||
try {
|
||||
await this.browser.close();
|
||||
// logger.log(`Scucesfully closed puppeteer and chrome`);
|
||||
} catch (exception) {
|
||||
// logger.log(`Failed to close puppeteer: ${exception}`);
|
||||
}
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `LaunchPuppeteer`;
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { loadProjectLabels } from '../labels';
|
||||
import { setTestLogName } from '../utils/logging';
|
||||
import {
|
||||
FrameworkTestContext,
|
||||
ReassignableFrameworkTestContext,
|
||||
TestProjectSpec,
|
||||
} from '../framework/frameworkTestSupport';
|
||||
import { LaunchProject } from '../fixtures/launchProject';
|
||||
import { PromiseOrNot } from '../testUtils';
|
||||
import { NullFixture } from '../fixtures/fixture';
|
||||
|
||||
/**
|
||||
* Extends the normal debug adapter context to include context relevant to puppeteer tests.
|
||||
*/
|
||||
export interface IPuppeteerTestContext extends FrameworkTestContext {
|
||||
/** The connected puppeteer browser object */
|
||||
browser: puppeteer.Browser | null;
|
||||
/** The currently running html page in Chrome */
|
||||
page: puppeteer.Page | null;
|
||||
launchProject: LaunchProject | null;
|
||||
}
|
||||
|
||||
export class PuppeteerTestContext extends ReassignableFrameworkTestContext {
|
||||
private _browser: puppeteer.Browser | null = null;
|
||||
private _page: puppeteer.Page | null = null;
|
||||
private _launchProject: LaunchProject | null = null;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public get browser(): puppeteer.Browser | null {
|
||||
return this._browser;
|
||||
}
|
||||
|
||||
public get page(): puppeteer.Page | null {
|
||||
return this._page;
|
||||
}
|
||||
|
||||
public get launchProject(): LaunchProject | null {
|
||||
return this._launchProject;
|
||||
}
|
||||
|
||||
public reassignTo(newWrapped: IPuppeteerTestContext): this {
|
||||
super.reassignTo(newWrapped);
|
||||
this._page = newWrapped.page;
|
||||
this._browser = newWrapped.browser;
|
||||
this._launchProject = newWrapped.launchProject;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a test with default settings and attach puppeteer. The test will start with the debug adapter
|
||||
* and chrome launched, and puppeteer attached.
|
||||
*
|
||||
* @param description Describe what this test should be testing
|
||||
* @param context The test context for this test sutie
|
||||
* @param testFunction The inner test function that will run a test using puppeteer
|
||||
*/
|
||||
function puppeteerTestFunction(
|
||||
description: string,
|
||||
context: PuppeteerTestContext,
|
||||
testFunction: (context: PuppeteerTestContext, page: puppeteer.Page) => PromiseOrNot<void>,
|
||||
functionToDeclareTest: Mocha.TestFunction | Mocha.ExclusiveTestFunction = test,
|
||||
): void {
|
||||
functionToDeclareTest(description, function() {
|
||||
return testFunction(context, context.page!);
|
||||
});
|
||||
}
|
||||
|
||||
puppeteerTestFunction.skip = (
|
||||
description: string,
|
||||
_context: PuppeteerTestContext,
|
||||
_testFunction: (context: IPuppeteerTestContext, page: puppeteer.Page) => Promise<any>,
|
||||
) =>
|
||||
test.skip(description, () => {
|
||||
throw new Error(`We don't expect this to be called`);
|
||||
});
|
||||
|
||||
puppeteerTestFunction.only = (
|
||||
description: string,
|
||||
context: PuppeteerTestContext,
|
||||
testFunction: (context: IPuppeteerTestContext, page: puppeteer.Page) => Promise<any>,
|
||||
) => puppeteerTestFunction(description, context, testFunction, test.only);
|
||||
|
||||
export const puppeteerTest = puppeteerTestFunction;
|
||||
|
||||
/**
|
||||
* Defines a custom test suite which will:
|
||||
* 1) automatically launch a server from a test project directory,
|
||||
* 2) launch the debug adapter (with chrome)
|
||||
*
|
||||
* From there, consumers can either launch a puppeteer instrumented test, or a normal test (i.e. without puppeteer) using
|
||||
* the test methods defined here, and can get access to the relevant variables.
|
||||
*
|
||||
* @param description Description for the mocha test suite
|
||||
* @param testSpec Info about the test project on which this suite will be based
|
||||
* @param callback The inner test suite that uses this context
|
||||
*/
|
||||
function puppeteerSuiteFunction(
|
||||
description: string,
|
||||
testSpec: TestProjectSpec,
|
||||
callback: (suiteContext: PuppeteerTestContext) => void,
|
||||
suiteFunctionToUse:
|
||||
| Mocha.SuiteFunction
|
||||
| Mocha.ExclusiveSuiteFunction
|
||||
| Mocha.PendingSuiteFunction = suite,
|
||||
): Mocha.ISuite | void {
|
||||
return suiteFunctionToUse(description, () => {
|
||||
const testContext = new PuppeteerTestContext();
|
||||
let fixture: LaunchProject | NullFixture = new NullFixture(); // This variable is shared across all test of this suite
|
||||
|
||||
setup(async function() {
|
||||
setTestLogName(this.currentTest!.fullTitle());
|
||||
const breakpointLabels = await loadProjectLabels(testSpec.props.webRoot);
|
||||
const launchProject = (fixture = await LaunchProject.launch(this, testSpec));
|
||||
|
||||
testContext.reassignTo({
|
||||
testSpec,
|
||||
debugClient: launchProject.debugClient,
|
||||
breakpointLabels,
|
||||
browser: launchProject.browser,
|
||||
page: launchProject.page,
|
||||
launchProject,
|
||||
});
|
||||
});
|
||||
|
||||
teardown(async () => {
|
||||
await fixture.cleanUp();
|
||||
fixture = new NullFixture();
|
||||
// logger.log(`teardown finished`);
|
||||
});
|
||||
|
||||
callback(testContext);
|
||||
});
|
||||
}
|
||||
|
||||
puppeteerSuiteFunction.skip = (
|
||||
description: string,
|
||||
testSpec: TestProjectSpec,
|
||||
callback: (suiteContext: PuppeteerTestContext) => any,
|
||||
) => puppeteerSuiteFunction(description, testSpec, callback, suite.skip);
|
||||
|
||||
puppeteerSuiteFunction.only = (
|
||||
description: string,
|
||||
testSpec: TestProjectSpec,
|
||||
callback: (suiteContext: PuppeteerTestContext) => any,
|
||||
) => puppeteerSuiteFunction(description, testSpec, callback, suite.only);
|
||||
|
||||
puppeteerSuiteFunction.skip = (
|
||||
description: string,
|
||||
_testSpec: TestProjectSpec,
|
||||
_callback: (suiteContext: PuppeteerTestContext) => any,
|
||||
) => suite.skip(description, () => {});
|
||||
|
||||
export const puppeteerSuite = puppeteerSuiteFunction;
|
|
@ -1,81 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
* Functions that make puppeteer testing easier
|
||||
*/
|
||||
|
||||
import request from 'request-promise-native';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
/**
|
||||
* Connect puppeteer to a currently running instance of chrome
|
||||
* @param port The port on which the chrome debugger is running
|
||||
*/
|
||||
export async function connectPuppeteer(port: number): Promise<puppeteer.Browser> {
|
||||
const resp = await request(`http://localhost:${port}/json/version`);
|
||||
const { webSocketDebuggerUrl } = JSON.parse(resp);
|
||||
|
||||
const browser = await (puppeteer as any).connect({
|
||||
browserWSEndpoint: webSocketDebuggerUrl,
|
||||
defaultViewport: null,
|
||||
});
|
||||
// logger.log(`Connected puppeteer on port: ${port} and websocket: ${JSON.stringify(webSocketDebuggerUrl)}`); // TODO@rob
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch puppeteer and a new instance of chrome
|
||||
* @param port The port on which the chrome debugger is running
|
||||
*/
|
||||
export async function launchPuppeteer(
|
||||
port: number,
|
||||
chromeArgs: string[] = [],
|
||||
): Promise<puppeteer.Browser> {
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
defaultViewport: null,
|
||||
args: chromeArgs.concat([`--remote-debugging-port=${port}`]),
|
||||
});
|
||||
// logger.log(`Launched puppeteer on port: ${port} and args: ${JSON.stringify(chromeArgs)}`);
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first (or only) page loaded in chrome
|
||||
* @param browser Puppeteer browser object
|
||||
*/
|
||||
export async function firstPage(browser: puppeteer.Browser): Promise<puppeteer.Page> {
|
||||
return (await browser.pages())[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a page in the browser by the url
|
||||
* @param browser Puppeteer browser object
|
||||
* @param url The url of the desired page
|
||||
* @param timeout Timeout in milliseconds
|
||||
*/
|
||||
export async function getPageByUrl(
|
||||
browser: puppeteer.Browser,
|
||||
url: string,
|
||||
timeout = 5000,
|
||||
): Promise<puppeteer.Page> {
|
||||
const before = new Date().getTime();
|
||||
let current = before;
|
||||
|
||||
// poll for the desired page url. If we don't find it within the timeout, throw an error
|
||||
while (current - before < timeout) {
|
||||
const pages = await browser.pages();
|
||||
const desiredPage = pages.find(p => p.url().toLowerCase() === url.toLowerCase());
|
||||
if (desiredPage) {
|
||||
return desiredPage;
|
||||
}
|
||||
|
||||
// TODO: yuck, clean up
|
||||
await new Promise((a, _r) => setTimeout(() => a(), timeout / 10));
|
||||
current = new Date().getTime();
|
||||
}
|
||||
throw `Page with url: ${url} could not be found within ${timeout}ms`;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
// Needed to make @types/puppeteer pass type checking
|
||||
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24419
|
||||
|
||||
interface Element {}
|
|
@ -1,4 +0,0 @@
|
|||
// Needed to make @types/puppeteer pass type checking
|
||||
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/24419
|
||||
|
||||
interface Element {}
|
|
@ -1,13 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
import * as testSetup from '../testSetup';
|
||||
import { TestProjectSpec } from '../framework/frameworkTestSupport';
|
||||
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
const REACT_PROJECT_ROOT = path.join(DATA_ROOT, 'react', 'dist');
|
||||
export const reactTestSpecification = new TestProjectSpec({ projectRoot: REACT_PROJECT_ROOT });
|
||||
export const reactWithLoopTestSpecification = new TestProjectSpec({
|
||||
projectRoot: path.join(DATA_ROOT, 'react_with_loop', 'dist'),
|
||||
});
|
|
@ -1,295 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import * as testSetup from './testSetup';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { FrameworkTestContext, TestProjectSpec } from './framework/frameworkTestSupport';
|
||||
import { BreakpointsWizard } from './wizards/breakpoints/breakpointsWizard';
|
||||
import { ExpectedFrame } from './wizards/breakpoints/implementation/stackTraceObjectAssertions';
|
||||
import { puppeteerSuite, puppeteerTest } from './puppeteer/puppeteerSuite';
|
||||
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
const SIMPLE_PROJECT_ROOT = path.join(DATA_ROOT, 'stackTrace');
|
||||
const TEST_SPEC = new TestProjectSpec({
|
||||
projectRoot: SIMPLE_PROJECT_ROOT,
|
||||
projectSrc: SIMPLE_PROJECT_ROOT,
|
||||
});
|
||||
|
||||
interface StackTraceValidationConfig {
|
||||
suiteContext: FrameworkTestContext;
|
||||
page: puppeteer.Page;
|
||||
breakPointLabel: string;
|
||||
buttonIdToClick: string;
|
||||
format?: DebugProtocol.StackFrameFormat;
|
||||
expectedFrames: ExpectedFrame[];
|
||||
}
|
||||
|
||||
async function validateStackTrace(config: StackTraceValidationConfig): Promise<void> {
|
||||
const incBtn = await config.page.waitForSelector(config.buttonIdToClick);
|
||||
|
||||
const breakpoints = BreakpointsWizard.create(config.suiteContext.debugClient, TEST_SPEC);
|
||||
const breakpointWizard = breakpoints.at('app.js');
|
||||
|
||||
const setStateBreakpoint = await breakpointWizard.breakpoint({
|
||||
text: "console.log('Test stack trace here')",
|
||||
});
|
||||
|
||||
await setStateBreakpoint.assertIsHitThenResumeWhen(() => incBtn.click(), {
|
||||
stackTrace: config.expectedFrames,
|
||||
stackFrameFormat: config.format,
|
||||
});
|
||||
}
|
||||
|
||||
puppeteerSuite('Stack Traces', TEST_SPEC, suiteContext => {
|
||||
// Need attach mode
|
||||
puppeteerTest(
|
||||
'Stack trace is generated with no formatting',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
await validateStackTrace({
|
||||
suiteContext: suiteContext,
|
||||
page: page,
|
||||
breakPointLabel: 'stackTraceBreakpoint',
|
||||
buttonIdToClick: '#button',
|
||||
format: {},
|
||||
expectedFrames: [
|
||||
{
|
||||
name: '<anonymous>',
|
||||
line: 11,
|
||||
column: 9,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'evalCallback',
|
||||
line: 12,
|
||||
column: 7,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: '<anonymous>',
|
||||
line: 1,
|
||||
column: 1,
|
||||
source: { evalCode: true },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'timeoutCallback',
|
||||
line: 6,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{ name: 'setTimeout', presentationHint: 'label' },
|
||||
{
|
||||
name: 'buttonClick',
|
||||
line: 2,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'onclick',
|
||||
line: 7,
|
||||
column: 49,
|
||||
source: { url: suiteContext.launchProject!.url },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
puppeteerTest(
|
||||
'Stack trace is generated with module formatting',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const url = suiteContext.launchProject!.url;
|
||||
await validateStackTrace({
|
||||
suiteContext: suiteContext,
|
||||
page: page,
|
||||
breakPointLabel: 'stackTraceBreakpoint',
|
||||
buttonIdToClick: '#button',
|
||||
format: {
|
||||
module: true,
|
||||
},
|
||||
expectedFrames: [
|
||||
{
|
||||
name: '(anonymous function) [app.js]',
|
||||
line: 11,
|
||||
column: 9,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'evalCallback [app.js]',
|
||||
line: 12,
|
||||
column: 7,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: /\(eval code\) \[.*VM.*]/,
|
||||
line: 1,
|
||||
column: 1,
|
||||
source: { evalCode: true },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'timeoutCallback [app.js]',
|
||||
line: 6,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{ name: '[ setTimeout ]', presentationHint: 'label' },
|
||||
{
|
||||
name: 'buttonClick [app.js]',
|
||||
line: 2,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: `onclick [${url.host}]`,
|
||||
line: 7,
|
||||
column: 49,
|
||||
source: { url },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
puppeteerTest(
|
||||
'Stack trace is generated with line formatting',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
await validateStackTrace({
|
||||
suiteContext: suiteContext,
|
||||
page: page,
|
||||
breakPointLabel: 'stackTraceBreakpoint',
|
||||
buttonIdToClick: '#button',
|
||||
format: {
|
||||
line: true,
|
||||
},
|
||||
expectedFrames: [
|
||||
{
|
||||
name: '(anonymous function) Line 11',
|
||||
line: 11,
|
||||
column: 9,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'evalCallback Line 12',
|
||||
line: 12,
|
||||
column: 7,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: '(eval code) Line 1',
|
||||
line: 1,
|
||||
column: 1,
|
||||
source: { evalCode: true },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'timeoutCallback Line 6',
|
||||
line: 6,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{ name: '[ setTimeout ]', presentationHint: 'label' },
|
||||
{
|
||||
name: 'buttonClick Line 2',
|
||||
line: 2,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'onclick Line 7',
|
||||
line: 7,
|
||||
column: 49,
|
||||
source: { url: suiteContext.launchProject!.url },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
puppeteerTest(
|
||||
'Stack trace is generated with all formatting',
|
||||
suiteContext,
|
||||
async (_context, page) => {
|
||||
const url = suiteContext.launchProject!.url;
|
||||
await validateStackTrace({
|
||||
suiteContext: suiteContext,
|
||||
page: page,
|
||||
breakPointLabel: 'stackTraceBreakpoint',
|
||||
buttonIdToClick: '#button',
|
||||
format: {
|
||||
parameters: true,
|
||||
parameterTypes: true,
|
||||
parameterNames: true,
|
||||
line: true,
|
||||
module: true,
|
||||
},
|
||||
expectedFrames: [
|
||||
{
|
||||
name: '(anonymous function) [app.js] Line 11',
|
||||
line: 11,
|
||||
column: 9,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'evalCallback [app.js] Line 12',
|
||||
line: 12,
|
||||
column: 7,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: /\(eval code\) \[.*VM.*] Line 1/,
|
||||
line: 1,
|
||||
column: 1,
|
||||
source: { evalCode: true },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: 'timeoutCallback [app.js] Line 6',
|
||||
line: 6,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{ name: '[ setTimeout ]', presentationHint: 'label' },
|
||||
{
|
||||
name: 'buttonClick [app.js] Line 2',
|
||||
line: 2,
|
||||
column: 5,
|
||||
source: { fileRelativePath: 'app.js' },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
{
|
||||
name: `onclick [${url.host}] Line 7`,
|
||||
line: 7,
|
||||
column: 49,
|
||||
source: { url },
|
||||
presentationHint: 'normal',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -1,130 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { createServer } from 'http-server';
|
||||
|
||||
import * as testSetup from './testSetup';
|
||||
import { HttpOrHttpsServer } from './types/server';
|
||||
import { ExtendedDebugClient } from './testSupport/debugClient';
|
||||
|
||||
suite('Stepping', () => {
|
||||
const DATA_ROOT = testSetup.DATA_ROOT;
|
||||
|
||||
let dc: ExtendedDebugClient;
|
||||
setup(function() {
|
||||
return testSetup.setup(this).then(_dc => (dc = _dc));
|
||||
});
|
||||
|
||||
let server: HttpOrHttpsServer | null;
|
||||
teardown(async () => {
|
||||
if (server) {
|
||||
server.close(err => console.log('Error closing server in teardown: ' + (err && err.message)));
|
||||
server = null;
|
||||
}
|
||||
|
||||
await testSetup.teardown();
|
||||
});
|
||||
|
||||
suite.skip('skipFiles', () => {
|
||||
test('when generated script is skipped via regex, the source can be un-skipped', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-merged-files');
|
||||
const sourceA = path.join(testProjectRoot, 'sourceA.ts');
|
||||
const sourceB2 = path.join(testProjectRoot, 'sourceB2.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
// Skip the full B generated script via launch config
|
||||
const bpLineA = 6;
|
||||
const skipFiles = ['b.js'];
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, skipFiles, webRoot: testProjectRoot },
|
||||
{ path: sourceA, line: bpLineA },
|
||||
);
|
||||
|
||||
// Step in, verify B sources are skipped
|
||||
await dc.stepInRequest();
|
||||
await dc.assertStoppedLocation('step', { path: sourceA, line: 2 });
|
||||
await dc.send('toggleSkipFileStatus', { path: sourceB2 });
|
||||
|
||||
// Continue back to sourceA, step in, should skip B1 and land on B2
|
||||
await dc.continueRequest();
|
||||
await dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA });
|
||||
await dc.stepInRequest();
|
||||
await dc.assertStoppedLocation('step', { path: sourceB2, line: 2 });
|
||||
});
|
||||
|
||||
test('when a non-sourcemapped script is skipped via regex, it can be unskipped', async () => {
|
||||
// Using this program, but run with sourcemaps disabled
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-sourcemapped-files');
|
||||
const sourceA = path.join(testProjectRoot, 'out/sourceA.js');
|
||||
const sourceB = path.join(testProjectRoot, 'out/sourceB.js');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
// Skip the full B generated script via launch config
|
||||
const skipFiles = ['sourceB.js'];
|
||||
const bpLineA = 5;
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, sourceMaps: false, skipFiles, webRoot: testProjectRoot },
|
||||
{ path: sourceA, line: bpLineA },
|
||||
);
|
||||
|
||||
// Step in, verify B sources are skipped
|
||||
await dc.stepInRequest();
|
||||
await dc.assertStoppedLocation('step', { path: sourceA, line: 2 });
|
||||
await dc.send('toggleSkipFileStatus', { path: sourceB });
|
||||
|
||||
// Continue back to A, step in, should land in B
|
||||
await dc.continueRequest();
|
||||
await dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA });
|
||||
await dc.stepInRequest();
|
||||
await dc.assertStoppedLocation('step', { path: sourceB, line: 2 });
|
||||
});
|
||||
|
||||
test('skip statuses for sourcemapped files are persisted across page reload', async () => {
|
||||
const testProjectRoot = path.join(DATA_ROOT, 'calls-between-merged-files');
|
||||
const sourceA = path.join(testProjectRoot, 'sourceA.ts');
|
||||
const sourceB2 = path.join(testProjectRoot, 'sourceB2.ts');
|
||||
|
||||
server = createServer({ root: testProjectRoot });
|
||||
server.listen(7890);
|
||||
|
||||
const url = 'http://localhost:7890/index.html';
|
||||
|
||||
// Skip the full B generated script via launch config
|
||||
const bpLineA = 6;
|
||||
const skipFiles = ['b.js'];
|
||||
await dc.hitBreakpointUnverified(
|
||||
{ url, skipFiles, webRoot: testProjectRoot },
|
||||
{ path: sourceA, line: bpLineA },
|
||||
);
|
||||
await Promise.all([dc.stepInRequest(), dc.waitForEvent('stopped')]);
|
||||
|
||||
// Un-skip b2 and refresh the page
|
||||
await Promise.all([
|
||||
// Wait for extra pause event sent after toggling skip status
|
||||
dc.waitForEvent('stopped'),
|
||||
dc.send('toggleSkipFileStatus', { path: sourceB2 }),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
dc.send('restart'),
|
||||
dc.assertStoppedLocation('breakpoint', { path: sourceA, line: bpLineA }),
|
||||
]);
|
||||
|
||||
// Persisted bp should be hit. Step in, and assert we stepped through B1 into B2
|
||||
await Promise.all([
|
||||
dc.stepInRequest(),
|
||||
dc.assertStoppedLocation('step', { path: sourceB2, line: 2 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"reporterEnabled": "node_modules/vscode-chrome-debug-core-testsupport/out/loggingReporter.js, mocha-junit-reporter"
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { logCallsTo, getDebugAdapterLogFilePath, setTestLogName } from './utils/logging';
|
||||
import { IBeforeAndAfterContext, ITestCallbackContext } from 'mocha';
|
||||
import { killAllChrome } from './testUtils';
|
||||
import { DefaultTimeoutMultiplier } from './utils/waitUntilReadyWithTimeout';
|
||||
import { IChromeLaunchConfiguration, chromeLaunchConfigDefaults } from '../configuration';
|
||||
import { ExtendedDebugClient } from './testSupport/debugClient';
|
||||
import * as testSupportSetup from './testSupport/testSetup';
|
||||
import { startDebugServer } from '../debugServer';
|
||||
import { IDisposable } from '../common/disposable';
|
||||
import getPort from 'get-port';
|
||||
|
||||
let testLaunchProps: (IChromeLaunchConfiguration & Dictionary<unknown>) | undefined; // TODO@rob i don't know
|
||||
|
||||
export const isThisV2 = true;
|
||||
export const isThisV1 = !isThisV2;
|
||||
export const isWindows = process.platform === 'win32';
|
||||
|
||||
function formLaunchArgs(
|
||||
launchArgs: IChromeLaunchConfiguration & Dictionary<unknown>,
|
||||
testTitle: string,
|
||||
): void {
|
||||
launchArgs.type = 'pwa-chrome' as any; // TODO@rob
|
||||
launchArgs.logging = { dap: '/tmp/dap.log', cdp: '/tmp/cdp.log' };
|
||||
launchArgs.sourceMapPathOverrides = {};
|
||||
launchArgs.trace = true;
|
||||
launchArgs.logTimestamps = true;
|
||||
launchArgs.disableNetworkCache = true;
|
||||
launchArgs.logFilePath = getDebugAdapterLogFilePath(testTitle);
|
||||
|
||||
if (!launchArgs.runtimeExecutable) {
|
||||
launchArgs.runtimeExecutable = puppeteer.executablePath();
|
||||
}
|
||||
|
||||
const hideWindows = process.env['TEST_DA_HIDE_WINDOWS'] === 'true';
|
||||
if (hideWindows) {
|
||||
launchArgs.runtimeArgs = ['--headless', '--disable-gpu'];
|
||||
}
|
||||
|
||||
// Start with a clean userDataDir for each test run
|
||||
const tmpDir = tmp.dirSync({ prefix: 'chrome2-' });
|
||||
launchArgs.userDataDir = tmpDir.name;
|
||||
if (testLaunchProps) {
|
||||
for (const key in testLaunchProps) {
|
||||
launchArgs[key] = testLaunchProps[key];
|
||||
}
|
||||
testLaunchProps = undefined;
|
||||
}
|
||||
|
||||
const argsWithDefaults = { ...chromeLaunchConfigDefaults, ...launchArgs };
|
||||
Object.assign(launchArgs, argsWithDefaults);
|
||||
}
|
||||
|
||||
let storedLaunchArgs: Partial<IChromeLaunchConfiguration> = {};
|
||||
|
||||
export function launchArgs(): Partial<IChromeLaunchConfiguration> {
|
||||
return { ...storedLaunchArgs };
|
||||
}
|
||||
|
||||
function patchLaunchArgs(launchArgs: IChromeLaunchConfiguration, testTitle: string): void {
|
||||
formLaunchArgs(launchArgs as any, testTitle); // TODO@rob
|
||||
storedLaunchArgs = launchArgs;
|
||||
}
|
||||
|
||||
export const lowercaseDriveLetterDirname = __dirname.charAt(0).toLowerCase() + __dirname.substr(1);
|
||||
export const PROJECT_ROOT = path.join(lowercaseDriveLetterDirname, '../../../');
|
||||
export const DATA_ROOT = path.join(PROJECT_ROOT, 'testdata/');
|
||||
|
||||
/** Default setup for all our tests, using the context of the setup method
|
||||
* - Best practise: The new best practise is to use the DefaultFixture when possible instead of calling this method directly
|
||||
*/
|
||||
export async function setup(
|
||||
context: IBeforeAndAfterContext | ITestCallbackContext,
|
||||
launchProps?: Partial<IChromeLaunchConfiguration>,
|
||||
): Promise<ExtendedDebugClient> {
|
||||
const currentTest = _.defaultTo(context.currentTest, context.test);
|
||||
return setupWithTitle(currentTest.fullTitle(), launchProps);
|
||||
}
|
||||
|
||||
let currentServer: IDisposable | undefined;
|
||||
|
||||
/** Default setup for all our tests, using the test title
|
||||
* - Best practise: The new best practise is to use the DefaultFixture when possible instead of calling this method directly
|
||||
*/
|
||||
export async function setupWithTitle(
|
||||
testTitle: string,
|
||||
launchProps?: Partial<IChromeLaunchConfiguration>,
|
||||
): Promise<ExtendedDebugClient> {
|
||||
// killAllChromesOnWin32(); // Kill chrome.exe instances before the tests. Killing them after the tests is not as reliable. If setup fails, teardown is not executed.
|
||||
setTestLogName(testTitle);
|
||||
|
||||
if (launchProps) {
|
||||
testLaunchProps = launchProps as any; // TODO@rob
|
||||
}
|
||||
|
||||
const port = await getPort();
|
||||
currentServer = await startDebugServer(port); // TODO@rob
|
||||
const debugClient = await testSupportSetup.setup({
|
||||
type: 'pwa-chrome',
|
||||
patchLaunchArgs: args => patchLaunchArgs(args, testTitle),
|
||||
port,
|
||||
});
|
||||
debugClient.defaultTimeout = DefaultTimeoutMultiplier * 10000 /*10 seconds*/;
|
||||
|
||||
const wrappedDebugClient = logCallsTo(debugClient, 'DebugAdapterClient');
|
||||
return wrappedDebugClient;
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
if (currentServer) {
|
||||
currentServer.dispose();
|
||||
}
|
||||
|
||||
await testSupportSetup.teardown();
|
||||
}
|
||||
|
||||
export function killAllChromesOnWin32() {
|
||||
if (process.platform === 'win32') {
|
||||
// We only need to kill the chrome.exe instances on the Windows agent
|
||||
// TODO: Figure out a way to remove this
|
||||
killAllChrome();
|
||||
}
|
||||
}
|
|
@ -1,229 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
|
||||
import { DebugClient } from 'vscode-debugadapter-testsupport';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
|
||||
export const THREAD_ID = 1;
|
||||
|
||||
export function setBreakpointOnStart(
|
||||
dc: DebugClient,
|
||||
bps: DebugProtocol.SourceBreakpoint[],
|
||||
program: string,
|
||||
expLine?: number,
|
||||
expCol?: number,
|
||||
expVerified?: boolean,
|
||||
): Promise<void> {
|
||||
return dc
|
||||
.waitForEvent('initialized')
|
||||
.then(event => setBreakpoint(dc, bps, program, expLine, expCol, expVerified))
|
||||
.then(() => dc.configurationDoneRequest())
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
export function setBreakpoint(
|
||||
dc: DebugClient,
|
||||
bps: DebugProtocol.SourceBreakpoint[],
|
||||
program: string,
|
||||
expLine?: number,
|
||||
expCol?: number,
|
||||
expVerified?: boolean,
|
||||
): Promise<void> {
|
||||
return dc
|
||||
.setBreakpointsRequest({
|
||||
breakpoints: bps,
|
||||
source: { path: program },
|
||||
})
|
||||
.then(response => {
|
||||
const bp = response.body.breakpoints[0];
|
||||
|
||||
if (typeof expVerified === 'boolean')
|
||||
assert.equal(bp.verified, expVerified, 'breakpoint verification mismatch: verified');
|
||||
if (typeof expLine === 'number')
|
||||
assert.equal(bp.line, expLine, 'breakpoint verification mismatch: line');
|
||||
if (typeof expCol === 'number')
|
||||
assert.equal(bp.column, expCol, 'breakpoint verification mismatch: column');
|
||||
});
|
||||
}
|
||||
|
||||
export interface IExpectedStopLocation {
|
||||
path?: string;
|
||||
line?: number;
|
||||
column?: number;
|
||||
}
|
||||
|
||||
export class ExtendedDebugClient extends DebugClient {
|
||||
async toggleSkipFileStatus(aPath: string): Promise<DebugProtocol.Response> {
|
||||
const results = await Promise.all([
|
||||
this.send('toggleSkipFileStatus', { path: aPath }),
|
||||
this.waitForEvent('stopped'),
|
||||
]);
|
||||
|
||||
return results[0];
|
||||
}
|
||||
|
||||
async loadedSources(args: DebugProtocol.LoadedSourcesArguments): Promise<any> {
|
||||
const response = await this.send('loadedSources');
|
||||
return response.body;
|
||||
}
|
||||
|
||||
async breakpointLocations(
|
||||
args: DebugProtocol.BreakpointLocationsArguments,
|
||||
): Promise<DebugProtocol.BreakpointLocationsResponse['body']> {
|
||||
const response = await super.send('breakpointLocations', args);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
continueRequest(): Promise<DebugProtocol.ContinueResponse> {
|
||||
return super.continueRequest({ threadId: THREAD_ID });
|
||||
}
|
||||
|
||||
nextRequest(): Promise<DebugProtocol.NextResponse> {
|
||||
return super.nextRequest({ threadId: THREAD_ID });
|
||||
}
|
||||
|
||||
stepOutRequest(): Promise<DebugProtocol.StepOutResponse> {
|
||||
return super.stepOutRequest({ threadId: THREAD_ID });
|
||||
}
|
||||
|
||||
stepInRequest(): Promise<DebugProtocol.StepInResponse> {
|
||||
return super.stepInRequest({ threadId: THREAD_ID });
|
||||
}
|
||||
|
||||
stackTraceRequest(): Promise<DebugProtocol.StackTraceResponse> {
|
||||
return super.stackTraceRequest({ threadId: THREAD_ID });
|
||||
}
|
||||
|
||||
continueAndStop(): Promise<any> {
|
||||
return Promise.all([
|
||||
super.continueRequest({ threadId: THREAD_ID }),
|
||||
this.waitForEvent('stopped'),
|
||||
]);
|
||||
}
|
||||
|
||||
nextAndStop(): Promise<any> {
|
||||
return Promise.all([super.nextRequest({ threadId: THREAD_ID }), this.waitForEvent('stopped')]);
|
||||
}
|
||||
|
||||
stepOutAndStop(): Promise<any> {
|
||||
return Promise.all([
|
||||
super.stepOutRequest({ threadId: THREAD_ID }),
|
||||
this.waitForEvent('stopped'),
|
||||
]);
|
||||
}
|
||||
|
||||
stepInAndStop(): Promise<any> {
|
||||
return Promise.all([
|
||||
super.stepInRequest({ threadId: THREAD_ID }),
|
||||
this.waitForEvent('stopped'),
|
||||
]);
|
||||
}
|
||||
|
||||
async continueTo(
|
||||
reason: string,
|
||||
expected: IExpectedStopLocation,
|
||||
): Promise<DebugProtocol.StackTraceResponse> {
|
||||
const results = await Promise.all([
|
||||
this.continueRequest(),
|
||||
this.assertStoppedLocation(reason, expected),
|
||||
]);
|
||||
|
||||
return results[1];
|
||||
}
|
||||
|
||||
async nextTo(
|
||||
reason: string,
|
||||
expected: IExpectedStopLocation,
|
||||
): Promise<DebugProtocol.StackTraceResponse> {
|
||||
const results = await Promise.all([
|
||||
this.nextRequest(),
|
||||
this.assertStoppedLocation(reason, expected),
|
||||
]);
|
||||
|
||||
return results[1] as any;
|
||||
}
|
||||
|
||||
async stepOutTo(
|
||||
reason: string,
|
||||
expected: IExpectedStopLocation,
|
||||
): Promise<DebugProtocol.StackTraceResponse> {
|
||||
const results = await Promise.all([
|
||||
this.stepOutRequest(),
|
||||
this.assertStoppedLocation(reason, expected),
|
||||
]);
|
||||
|
||||
return results[1] as any;
|
||||
}
|
||||
|
||||
async stepInTo(
|
||||
reason: string,
|
||||
expected: IExpectedStopLocation,
|
||||
): Promise<DebugProtocol.StackTraceResponse> {
|
||||
const results = await Promise.all([
|
||||
this.stepInRequest(),
|
||||
this.assertStoppedLocation(reason, expected),
|
||||
]);
|
||||
|
||||
return results[1] as any;
|
||||
}
|
||||
|
||||
waitForEvent(eventType: string): Promise<DebugProtocol.Event> {
|
||||
return super.waitForEvent(eventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a copy of DebugClient's hitBreakpoint, except that it doesn't assert 'verified' by default. In the Chrome debugger, a bp may be verified or unverified at launch,
|
||||
* depending on whether it's randomly received before or after the 'scriptParsed' event for its script. So we can't always check this prop.
|
||||
*/
|
||||
hitBreakpointUnverified(
|
||||
launchArgs: any,
|
||||
location: { path: string; line: number; column?: number; verified?: boolean },
|
||||
expected?: { path?: string; line?: number; column?: number; verified?: boolean },
|
||||
): Promise<any> {
|
||||
return Promise.all([
|
||||
this.waitForEvent('initialized')
|
||||
.then(event => {
|
||||
return this.setBreakpointsRequest({
|
||||
lines: [location.line],
|
||||
breakpoints: [{ line: location.line, column: location.column }],
|
||||
source: { path: location.path },
|
||||
});
|
||||
})
|
||||
.then(response => {
|
||||
if (response.body.breakpoints.length > 0) {
|
||||
const bp = response.body.breakpoints[0];
|
||||
|
||||
if (typeof location.verified === 'boolean') {
|
||||
assert.equal(
|
||||
bp.verified,
|
||||
location.verified,
|
||||
'breakpoint verification mismatch: verified',
|
||||
);
|
||||
}
|
||||
if (bp.source && bp.source.path) {
|
||||
this.assertPath(
|
||||
bp.source.path,
|
||||
location.path,
|
||||
'breakpoint verification mismatch: path',
|
||||
);
|
||||
}
|
||||
if (typeof bp.line === 'number') {
|
||||
assert.equal(bp.line, location.line, 'breakpoint verification mismatch: line');
|
||||
}
|
||||
if (typeof location.column === 'number' && typeof bp.column === 'number') {
|
||||
assert.equal(bp.column, location.column, 'breakpoint verification mismatch: column');
|
||||
}
|
||||
}
|
||||
|
||||
return this.configurationDoneRequest();
|
||||
}),
|
||||
|
||||
this.launch(launchArgs),
|
||||
|
||||
this.assertStoppedLocation('breakpoint', expected || location),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as mocha from 'mocha';
|
||||
import * as events from 'events';
|
||||
|
||||
class LoggingReporter extends mocha.reporters.Spec {
|
||||
static alwaysDumpLogs = false;
|
||||
static logEE = new events.EventEmitter();
|
||||
|
||||
private testLogs: string[] = [];
|
||||
private inTest = false;
|
||||
|
||||
constructor(runner: any) {
|
||||
super(runner);
|
||||
|
||||
LoggingReporter.logEE.on('log', msg => {
|
||||
if (this.inTest) {
|
||||
this.testLogs.push(msg);
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('test', () => {
|
||||
this.inTest = true;
|
||||
this.testLogs = [];
|
||||
});
|
||||
|
||||
runner.on('pass', () => {
|
||||
this.inTest = false;
|
||||
|
||||
if (LoggingReporter.alwaysDumpLogs) {
|
||||
this.dumpLogs();
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('fail', () => {
|
||||
this.inTest = false;
|
||||
this.dumpLogs();
|
||||
|
||||
// console.log(new Date().toISOString().split(/[TZ]/)[1] + ' Finished'); // TODO@rob
|
||||
});
|
||||
}
|
||||
|
||||
private dumpLogs(): void {
|
||||
this.testLogs.forEach(msg => {
|
||||
console.log(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export = LoggingReporter;
|
|
@ -1,120 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
|
||||
import { ExtendedDebugClient } from './debugClient';
|
||||
|
||||
// ES6 default export...
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const LoggingReporter = require('./loggingReporter');
|
||||
// LoggingReporter.alwaysDumpLogs = true;
|
||||
|
||||
let unhandledAdapterErrors: string[];
|
||||
const origTest = test;
|
||||
const checkLogTest = (
|
||||
title: string,
|
||||
testCallback?: any,
|
||||
testFn: Function = origTest,
|
||||
): Mocha.ITest => {
|
||||
// Hack to always check logs after a test runs, can simplify after this issue:
|
||||
// https://github.com/mochajs/mocha/issues/1635
|
||||
if (!testCallback) {
|
||||
return origTest(title, testCallback);
|
||||
}
|
||||
|
||||
function runTest(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const optionalCallback = (e: Error) => {
|
||||
if (e) reject(e);
|
||||
else resolve();
|
||||
};
|
||||
|
||||
const maybeP = testCallback(optionalCallback);
|
||||
if (maybeP && maybeP.then) {
|
||||
maybeP.then(resolve, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return testFn(title, () => {
|
||||
return runTest().then(() => {
|
||||
// If any unhandled errors were logged, then ensure the test fails
|
||||
if (unhandledAdapterErrors.length) {
|
||||
const errStr =
|
||||
unhandledAdapterErrors.length === 1
|
||||
? unhandledAdapterErrors[0]
|
||||
: JSON.stringify(unhandledAdapterErrors);
|
||||
throw new Error(errStr);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
(<Mocha.ITestDefinition>checkLogTest).only = (expectation, assertion) =>
|
||||
checkLogTest(expectation, assertion, origTest.only);
|
||||
(<Mocha.ITestDefinition>checkLogTest).skip = test.skip;
|
||||
test = <any>checkLogTest;
|
||||
|
||||
function log(e: DebugProtocol.OutputEvent): void {
|
||||
// Skip telemetry events
|
||||
if (e.body.category === 'telemetry') return;
|
||||
|
||||
if (!e.body.output) return; // TODO@rob
|
||||
|
||||
const timestamp = new Date().toISOString().split(/[TZ]/)[1];
|
||||
const outputBody = e.body.output
|
||||
? e.body.output.trim()
|
||||
: 'variablesReference: ' + e.body.variablesReference;
|
||||
const msg = ` ${timestamp} ${outputBody}`;
|
||||
LoggingReporter.logEE.emit('log', msg);
|
||||
|
||||
if (msg.indexOf('********') >= 0) unhandledAdapterErrors.push(msg);
|
||||
}
|
||||
|
||||
export type PatchLaunchArgsCb = (launchArgs: any) => Promise<void> | void;
|
||||
|
||||
let dc: ExtendedDebugClient;
|
||||
function patchLaunchFn(patchLaunchArgsCb: PatchLaunchArgsCb): void {
|
||||
function patchLaunchArgs(launchArgs: any): Promise<void> {
|
||||
launchArgs.request = 'launch';
|
||||
launchArgs.trace = 'verbose';
|
||||
const patchReturnVal = patchLaunchArgsCb(launchArgs);
|
||||
return patchReturnVal || Promise.resolve();
|
||||
}
|
||||
|
||||
const origLaunch = dc.launch;
|
||||
dc.launch = (launchArgs: any) => {
|
||||
return patchLaunchArgs(launchArgs).then(() => origLaunch.call(dc, launchArgs));
|
||||
};
|
||||
|
||||
const origAttachRequest = dc.attachRequest;
|
||||
dc.attachRequest = (attachArgs: any) => {
|
||||
return patchLaunchArgs(attachArgs).then(() => origAttachRequest.call(dc, attachArgs));
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISetupOpts {
|
||||
type: string;
|
||||
patchLaunchArgs?: PatchLaunchArgsCb;
|
||||
port?: number;
|
||||
alwaysDumpLogs?: boolean;
|
||||
}
|
||||
|
||||
export function setup(opts: ISetupOpts): Promise<ExtendedDebugClient> {
|
||||
unhandledAdapterErrors = [];
|
||||
dc = new ExtendedDebugClient('node', '', opts.type); // Will always use 'port'
|
||||
if (opts.patchLaunchArgs) {
|
||||
patchLaunchFn(opts.patchLaunchArgs);
|
||||
}
|
||||
|
||||
LoggingReporter.alwaysDumpLogs = opts.alwaysDumpLogs;
|
||||
dc.addListener('output', log);
|
||||
|
||||
return dc.start(opts.port).then(() => dc);
|
||||
}
|
||||
|
||||
export async function teardown(): Promise<void> {
|
||||
dc.removeListener('output', log);
|
||||
await dc.stop();
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
const puppeteer = require('puppeteer');
|
||||
import { IChromeLaunchConfiguration, IChromeAttachConfiguration } from '../configuration';
|
||||
|
||||
export function setupUnhandledRejectionListener(): void {
|
||||
process.addListener('unhandledRejection', unhandledRejectionListener);
|
||||
}
|
||||
|
||||
export function removeUnhandledRejectionListener(): void {
|
||||
process.removeListener('unhandledRejection', unhandledRejectionListener);
|
||||
}
|
||||
|
||||
function unhandledRejectionListener(reason: any, _p: Promise<any>) {
|
||||
console.log('*');
|
||||
console.log('**');
|
||||
console.log('***');
|
||||
console.log('****');
|
||||
console.log('*****');
|
||||
console.log(
|
||||
`ERROR!! Unhandled promise rejection, a previous test may have failed but reported success.`,
|
||||
);
|
||||
console.log(reason.toString());
|
||||
console.log('*****');
|
||||
console.log('****');
|
||||
console.log('***');
|
||||
console.log('**');
|
||||
console.log('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* path.resolve + fixing the drive letter to match what VS Code does. Basically tests can use this when they
|
||||
* want to force a path to native slashes and the correct letter case, but maybe can't use un-mocked utils.
|
||||
*/
|
||||
export function pathResolve(...segments: string[]): string {
|
||||
let aPath = path.resolve.apply(null, segments);
|
||||
|
||||
if (aPath.match(/^[A-Za-z]:/)) {
|
||||
aPath = aPath[0].toLowerCase() + aPath.substr(1);
|
||||
}
|
||||
|
||||
return aPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills all running instances of chrome (that were launched by the tests, on Windows at least) on the test host
|
||||
*/
|
||||
export function killAllChrome() {
|
||||
try {
|
||||
const killCmd =
|
||||
process.platform === 'win32'
|
||||
? `start powershell -WindowStyle hidden -Command "Get-Process | Where-Object {$_.Path -like '*${puppeteer.executablePath()}*'} | Stop-Process"`
|
||||
: 'killall chrome';
|
||||
const hideWindows = process.env['TEST_DA_HIDE_WINDOWS'] === 'true';
|
||||
const output = execSync(killCmd, { windowsHide: hideWindows }); // TODO: windowsHide paramenter doesn't currently work. It might be related to this: https://github.com/nodejs/node/issues/21825
|
||||
if (output.length > 0) {
|
||||
// Don't print empty lines
|
||||
console.log(output.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error killing chrome: ${e.message}`);
|
||||
}
|
||||
// the kill command will exit with a non-zero code (and cause execSync to throw) if chrome is already stopped
|
||||
}
|
||||
|
||||
export const readFileP = util.promisify(fs.readFile);
|
||||
export const writeFileP = util.promisify(fs.writeFile);
|
||||
|
||||
export type PromiseOrNot<T> = T | Promise<T>;
|
||||
|
||||
export interface IDeferred<T> {
|
||||
resolve: (result: T) => void;
|
||||
reject: (err: Error) => void;
|
||||
promise: Promise<T>;
|
||||
}
|
||||
|
||||
export function getDeferred<T>(): Promise<IDeferred<T>> {
|
||||
return new Promise(r => {
|
||||
// Promise callback is called synchronously
|
||||
let resolve: IDeferred<T>['resolve'] = () => {
|
||||
throw new Error('Deferred was resolved before intialization');
|
||||
};
|
||||
let reject: IDeferred<T>['reject'] = () => {
|
||||
throw new Error('Deferred was rejected before initialization');
|
||||
};
|
||||
const promise = new Promise<T>((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
r({ resolve, reject, promise });
|
||||
});
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function promiseTimeout(
|
||||
p?: Promise<any>,
|
||||
timeoutMs = 1000,
|
||||
timeoutMsg?: string,
|
||||
): Promise<any> {
|
||||
if (timeoutMsg === undefined) {
|
||||
timeoutMsg = `Promise timed out after ${timeoutMs}ms`;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (p) {
|
||||
p.then(resolve, reject);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (p) {
|
||||
reject(new Error(timeoutMsg));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
export function retryAsync(
|
||||
fn: () => Promise<any>,
|
||||
timeoutMs: number,
|
||||
intervalDelay = 0,
|
||||
): Promise<any> {
|
||||
const startTime = Date.now();
|
||||
|
||||
function tryUntilTimeout(): Promise<any> {
|
||||
return fn().catch(e => {
|
||||
if (Date.now() - startTime < timeoutMs - intervalDelay) {
|
||||
return promiseTimeout(undefined, intervalDelay).then(tryUntilTimeout);
|
||||
} else {
|
||||
return errP(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return tryUntilTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for returning a rejected promise with an Error object. Avoids double-wrapping an Error, which could happen
|
||||
* when passing on a failure from a Promise error handler.
|
||||
* @param msg - Should be either a string or an Error
|
||||
*/
|
||||
export function errP(msg: string | Error): Promise<never> {
|
||||
const isErrorLike = (thing: any): thing is Error => !!thing.message;
|
||||
|
||||
let e: Error;
|
||||
if (!msg) {
|
||||
e = new Error('Unknown error');
|
||||
} else if (isErrorLike(msg)) {
|
||||
// msg is already an Error object
|
||||
e = msg;
|
||||
} else {
|
||||
e = new Error(msg);
|
||||
}
|
||||
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
export interface IChromeTestLaunchConfiguration extends Partial<IChromeLaunchConfiguration> {
|
||||
request: 'launch';
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IChromeTestAttachConfiguration extends Partial<IChromeAttachConfiguration> {
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a local path to a file URL, like
|
||||
* C:/code/app.js => file:///C:/code/app.js
|
||||
* /code/app.js => file:///code/app.js
|
||||
* \\code\app.js => file:///code/app.js
|
||||
*/
|
||||
export function pathToFileURL(_absPath: string, normalize?: boolean): string {
|
||||
let absPath = forceForwardSlashes(_absPath);
|
||||
if (isTrue(normalize)) {
|
||||
absPath = path.normalize(absPath);
|
||||
absPath = forceForwardSlashes(absPath);
|
||||
}
|
||||
|
||||
const filePrefix = _absPath.startsWith('\\\\')
|
||||
? 'file:/'
|
||||
: absPath.startsWith('/')
|
||||
? 'file://'
|
||||
: 'file:///';
|
||||
|
||||
absPath = filePrefix + absPath;
|
||||
return encodeURI(absPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace any backslashes with forward slashes
|
||||
* blah\something => blah/something
|
||||
*/
|
||||
function forceForwardSlashes(aUrl: string): string {
|
||||
return aUrl
|
||||
.replace(/\\\//g, '/') // Replace \/ (unnecessarily escaped forward slash)
|
||||
.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the parameter is defined and is true
|
||||
*/
|
||||
function isTrue(booleanOrUndefined: boolean | undefined): boolean {
|
||||
return booleanOrUndefined === true;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
export type HttpOrHttpsServer = http.Server | https.Server;
|
|
@ -1,34 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as _ from 'lodash';
|
||||
import {
|
||||
createColumnNumber,
|
||||
createLineNumber,
|
||||
LineNumber,
|
||||
} from '../core-v2/chrome/internal/locations/subtypes';
|
||||
import { Position } from '../core-v2/chrome/internal/locations/location';
|
||||
import { readFileP } from '../testUtils';
|
||||
|
||||
export async function findPositionOfTextInFile(filePath: string, text: string): Promise<Position> {
|
||||
const contentsIncludingCarriageReturns = await readFileP(filePath, 'utf8');
|
||||
const contents = contentsIncludingCarriageReturns.replace(/\r/g, '');
|
||||
const textStartIndex = contents.indexOf(text);
|
||||
|
||||
if (textStartIndex >= 0) {
|
||||
const textLineNumber = findLineNumber(contents, textStartIndex);
|
||||
const lastNewLineBeforeTextIndex = contents.lastIndexOf('\n', textStartIndex);
|
||||
const textColumNumber = createColumnNumber(textStartIndex - (lastNewLineBeforeTextIndex + 1));
|
||||
return new Position(textLineNumber, textColumNumber);
|
||||
} else {
|
||||
throw new Error(`Couldn't find ${text} in ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function findLineNumber(contents: string, characterIndex: number): LineNumber {
|
||||
const contentsBeforeCharacter = contents.substr(0, characterIndex);
|
||||
const textLineNumber = createLineNumber(
|
||||
_.countBy(contentsBeforeCharacter, c => c === '\n')['true'] || 0,
|
||||
);
|
||||
return textLineNumber;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as utils from '../testUtils';
|
||||
|
||||
/**
|
||||
* The test normally run very fast, so it's difficult to see what actions they are taking in the browser.
|
||||
* We can use the HumanSlownessSimulator to artifically slow some classes like the puppeteer classes, so it's the actions
|
||||
* will be taken at a lower speed, and it'll be easier to see and understand what is happening
|
||||
*/
|
||||
export class HumanSlownessSimulator {
|
||||
public constructor(
|
||||
private readonly _slownessInMillisecondsValueGenerator: () => number = () => 500,
|
||||
) {}
|
||||
|
||||
public simulateSlowness(): Promise<void> {
|
||||
return utils.promiseTimeout(undefined, this._slownessInMillisecondsValueGenerator());
|
||||
}
|
||||
|
||||
public wrap<T extends object>(object: T): T {
|
||||
return new HumanSpeedProxy(this, object).wrapped();
|
||||
}
|
||||
}
|
||||
|
||||
export class HumanSpeedProxy<T extends object> {
|
||||
constructor(
|
||||
private readonly _humanSlownessSimulator: HumanSlownessSimulator,
|
||||
private readonly _objectToWrap: T,
|
||||
) {}
|
||||
|
||||
public wrapped(): T {
|
||||
const handler = {
|
||||
get: <K extends keyof T>(target: T, propertyKey: K, _receiver: any) => {
|
||||
this._humanSlownessSimulator.simulateSlowness();
|
||||
const originalPropertyValue = target[propertyKey];
|
||||
if (typeof originalPropertyValue === 'function') {
|
||||
return (...args: any) => {
|
||||
const result = originalPropertyValue.apply(target, args);
|
||||
if (result && result.then) {
|
||||
// Currently we only slow down async operations
|
||||
return result.then(
|
||||
async (promiseResult: object) => {
|
||||
await this._humanSlownessSimulator.simulateSlowness();
|
||||
return typeof promiseResult === 'object'
|
||||
? this._humanSlownessSimulator.wrap(promiseResult)
|
||||
: promiseResult;
|
||||
},
|
||||
(rejection: unknown) => {
|
||||
return rejection;
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return originalPropertyValue;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return new Proxy<T>(this._objectToWrap, handler);
|
||||
}
|
||||
}
|
||||
|
||||
const humanSlownessSimulator = new HumanSlownessSimulator();
|
||||
|
||||
const humanSlownessEnabeld = process.env.RUN_TESTS_SLOWLY === 'true';
|
||||
|
||||
export function slowToHumanLevel<T extends object>(object: T): T {
|
||||
return humanSlownessEnabeld ? humanSlownessSimulator.wrap(object) : object;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
import {
|
||||
MethodsCalledLogger,
|
||||
IMethodsCalledLoggerConfiguration,
|
||||
MethodsCalledLoggerConfiguration,
|
||||
ReplacementInstruction,
|
||||
wrapWithMethodLogger,
|
||||
} from '../core-v2/chrome/logging/methodsCalledLogger';
|
||||
|
||||
const useDateTimeInLog = false;
|
||||
function dateTimeForFilePath(): string {
|
||||
return new Date()
|
||||
.toISOString()
|
||||
.replace(/:/g, '')
|
||||
.replace('T', ' ')
|
||||
.replace(/\.[0-9]+^/, '');
|
||||
}
|
||||
|
||||
function dateTimeForFilePathIfNeeded() {
|
||||
return useDateTimeInLog ? `-${dateTimeForFilePath()}` : '';
|
||||
}
|
||||
|
||||
const logsFolderPath = path.resolve(process.cwd(), 'logs');
|
||||
|
||||
export function getDebugAdapterLogFilePath(testTitle: string): string {
|
||||
return logFilePath(testTitle, 'DA');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a title to an equivalent title that can be used as a filename (We use this to convert the name of our tests into the name of the logfile for that test)
|
||||
*/
|
||||
function sanitizeTestTitle(testTitle: string) {
|
||||
return (
|
||||
testTitle
|
||||
.replace(/[:\/\\]/g, '-')
|
||||
|
||||
// These replacements are needed for the hit count breakpoint tests, which have these characters in their title
|
||||
.replace(/ > /g, ' bigger than ')
|
||||
.replace(/ < /g, ' smaller than ')
|
||||
.replace(/ >= /g, ' bigger than or equal to ')
|
||||
.replace(/ <= /g, ' smaller than or equal to ')
|
||||
);
|
||||
}
|
||||
|
||||
function logFilePath(testTitle: string, logType: string) {
|
||||
return path.join(
|
||||
logsFolderPath,
|
||||
`${process.platform}-${sanitizeTestTitle(
|
||||
testTitle,
|
||||
)}-${logType}${dateTimeForFilePathIfNeeded()}.log`,
|
||||
);
|
||||
}
|
||||
|
||||
// logger.init(() => { });
|
||||
|
||||
// // Dispose the logger on unhandled errors, so it'll flush the remaining contents of the log...
|
||||
// process.on('uncaughtException', () => logger.dispose());
|
||||
// process.on('unhandledRejection', () => logger.dispose());
|
||||
|
||||
const currentTestTitle = '';
|
||||
export function setTestLogName(testTitle: string): void {
|
||||
// We call setTestLogName in the common setup code. We want to call it earlier in puppeteer tests to get the logs even when the setup fails
|
||||
// So we write this code to be able to call it two times, and the second time will get ignored
|
||||
if (testTitle !== currentTestTitle) {
|
||||
// logger.setup(LogLevel.Verbose, logFilePath(testTitle, 'TEST'));
|
||||
testTitle = currentTestTitle;
|
||||
}
|
||||
}
|
||||
|
||||
class PuppeteerMethodsCalledLoggerConfiguration implements IMethodsCalledLoggerConfiguration {
|
||||
private readonly _wrapped = new MethodsCalledLoggerConfiguration('', []);
|
||||
public readonly replacements: ReplacementInstruction[] = [];
|
||||
|
||||
public customizeResult<T>(methodName: string | symbol | number, args: any, result: T): T {
|
||||
if (methodName === 'waitForSelector' && typeof result === 'object' && args.length >= 1) {
|
||||
return wrapWithMethodLogger(<T & object>result, args[0]);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public customizeArgumentsBeforeCall(
|
||||
receiverName: string,
|
||||
methodName: string | symbol | number,
|
||||
args: object[],
|
||||
): void {
|
||||
this._wrapped.customizeArgumentsBeforeCall(receiverName, methodName, args);
|
||||
}
|
||||
}
|
||||
|
||||
export function logCallsTo<T extends object>(object: T, name: string): T {
|
||||
return new MethodsCalledLogger(
|
||||
new PuppeteerMethodsCalledLoggerConfiguration(),
|
||||
object,
|
||||
name,
|
||||
).wrapped();
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { DebugClient } from 'vscode-debugadapter-testsupport';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
|
||||
export function onUnhandledException(
|
||||
client: DebugClient,
|
||||
actionWhenUnhandledException: (exceptionMessage: string) => void,
|
||||
): void {
|
||||
client.on('output', (args: DebugProtocol.OutputEvent) => {
|
||||
if (args.body.category === 'telemetry' && args.body.output === 'error') {
|
||||
actionWhenUnhandledException(
|
||||
`Debug adapter had an unhandled error: ${args.body.data.exceptionMessage}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function onHandledError(
|
||||
client: DebugClient,
|
||||
actionWhenHandledError: (exceptionMessage: string) => void,
|
||||
): void {
|
||||
client.on('output', (args: DebugProtocol.OutputEvent) => {
|
||||
if (args.body.category === 'stderr') {
|
||||
actionWhenHandledError(args.body.output);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
export async function asyncRepeatSerially(
|
||||
howManyTimes: number,
|
||||
action: () => Promise<void>,
|
||||
): Promise<void> {
|
||||
for (let index = 0; index < howManyTimes; ++index) {
|
||||
await action();
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import * as _ from 'lodash';
|
||||
import * as utils from '../testUtils';
|
||||
|
||||
// The VSTS agents run slower than our machines. Use this value to reduce proportinoally the timeouts in your dev machine
|
||||
export const DefaultTimeoutMultiplier = parseFloat(
|
||||
_.defaultTo(process.env['TEST_TIMEOUT_MULTIPLIER'], '1'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Wait until the isReady condition evaluates to true. This method will evaluate it every 50 milliseconds until it returns true. It will time-out after maxWaitTimeInMilliseconds milliseconds
|
||||
*/
|
||||
export async function waitUntilReadyWithTimeout(
|
||||
isReady: () => boolean,
|
||||
maxWaitTimeInMilliseconds = DefaultTimeoutMultiplier * 30000 /* 30 seconds */,
|
||||
) {
|
||||
const maximumDateTimeToWaitUntil = Date.now() + maxWaitTimeInMilliseconds;
|
||||
|
||||
while (!isReady() && Date.now() < maximumDateTimeToWaitUntil) {
|
||||
await utils.promiseTimeout(undefined, 10 /*ms*/);
|
||||
}
|
||||
|
||||
if (!isReady()) {
|
||||
throw new Error(
|
||||
`Timed-out after waiting for condition to be ready for ${maxWaitTimeInMilliseconds}ms. Condition: ${isReady}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { IVerificationsAndAction } from './breakpointsWizard';
|
||||
import { IBPActionWhenHit } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
|
||||
import { RemoveProperty } from '../../core-v2/typeUtils';
|
||||
import { Position } from '../../core-v2/chrome/internal/locations/location';
|
||||
|
||||
export class BreakpointWizard {
|
||||
private isBreakpointSet = false;
|
||||
|
||||
public constructor(
|
||||
private readonly _internal: InternalFileBreakpointsWizard,
|
||||
public readonly position: Position,
|
||||
public readonly actionWhenHit: IBPActionWhenHit,
|
||||
public readonly name: string,
|
||||
public readonly boundPosition: Position,
|
||||
) {}
|
||||
|
||||
public get filePath(): string {
|
||||
return this._internal.filePath;
|
||||
}
|
||||
|
||||
public async setThenWaitForVerifiedThenValidate(): Promise<BreakpointWizard> {
|
||||
await this.setWithoutVerifying();
|
||||
await this.waitUntilVerified();
|
||||
this.assertIsVerified();
|
||||
return this;
|
||||
}
|
||||
|
||||
public async waitUntilVerified(): Promise<BreakpointWizard> {
|
||||
this.validateIsSet('waitUntilVerified');
|
||||
await this._internal.waitUntilVerified(this);
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
public async setWithoutVerifying(): Promise<BreakpointWizard> {
|
||||
this.validateIsUnset('setWithoutVerifying');
|
||||
await this._internal.set(this);
|
||||
this.isBreakpointSet = true;
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
public async unset(): Promise<BreakpointWizard> {
|
||||
this.validateIsSet('unset');
|
||||
await this._internal.unset(this);
|
||||
this.isBreakpointSet = false;
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResumeWhen(
|
||||
lastActionToMakeBreakpointHit: () => Promise<unknown>,
|
||||
verifications: IVerificationsAndAction = {},
|
||||
): Promise<BreakpointWizard> {
|
||||
this.validateIsSet('assertIsHitThenResumeWhen');
|
||||
await this._internal.assertIsHitThenResumeWhen(
|
||||
this,
|
||||
lastActionToMakeBreakpointHit,
|
||||
verifications,
|
||||
);
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResume(
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<BreakpointWizard> {
|
||||
this.validateIsSet('assertIsHitThenResume');
|
||||
await this._internal.assertIsHitThenResume(this, verifications);
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
public assertIsVerified(): this {
|
||||
this.validateIsSet('assertIsVerified');
|
||||
this._internal.assertIsVerified(this);
|
||||
return this;
|
||||
}
|
||||
|
||||
public assertIsNotVerified(unverifiedReason: string): this {
|
||||
this.validateIsSet('assertIsNotVerified');
|
||||
this._internal.assertIsNotVerified(this, unverifiedReason);
|
||||
return this;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
private validateIsSet(operationName: string): void {
|
||||
this.validateInExpectedState(true, operationName);
|
||||
}
|
||||
|
||||
private validateIsUnset(operationName: string): void {
|
||||
this.validateInExpectedState(false, operationName);
|
||||
}
|
||||
|
||||
private validateInExpectedState(needsToBeSet: boolean, operationName: string): void {
|
||||
if (this.isBreakpointSet !== needsToBeSet) {
|
||||
throw new Error(
|
||||
`Can't perform operation ${operationName} because it needs breakpoint to ${
|
||||
needsToBeSet ? '' : 'NOT '
|
||||
} be set`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type VSCodeActionWhenHit = RemoveProperty<DebugProtocol.SourceBreakpoint, 'line' | 'column'>;
|
|
@ -1,178 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { PromiseOrNot } from '../../testUtils';
|
||||
import { IExpectedVariables, VariablesWizard } from '../variables/variablesWizard';
|
||||
import {
|
||||
ExpectedFrame,
|
||||
StackTraceObjectAssertions,
|
||||
} from './implementation/stackTraceObjectAssertions';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { ValidatedMap } from '../../core-v2/chrome/collections/validatedMap';
|
||||
import {
|
||||
InternalFileBreakpointsWizard,
|
||||
BreakpointStatusChangedWithId,
|
||||
} from './implementation/internalFileBreakpointsWizard';
|
||||
import { ExtendedDebugClient } from '../../testSupport/debugClient';
|
||||
import { PausedWizard } from '../pausedWizard';
|
||||
import { TestProjectSpec } from '../../framework/frameworkTestSupport';
|
||||
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
|
||||
import { FileBreakpointsWizard } from './fileBreakpointsWizard';
|
||||
import { BreakpointWizard } from './breakpointWizard';
|
||||
import { expect } from 'chai';
|
||||
import { stackTrace, StackFrameWizard } from '../variables/stackFrameWizard';
|
||||
import { assertMatchesBreakpointLocation } from './implementation/breakpointsAssertions';
|
||||
import { StackTraceStringAssertions } from './implementation/stackTraceStringAssertions';
|
||||
|
||||
export interface IVerificationsAndAction {
|
||||
action?: () => PromiseOrNot<void>;
|
||||
variables?: IExpectedVariables;
|
||||
stackTrace?: string | ExpectedFrame[];
|
||||
stackFrameFormat?: DebugProtocol.StackFrameFormat;
|
||||
}
|
||||
|
||||
export class BreakpointsWizard {
|
||||
private readonly _variablesWizard = new VariablesWizard(this._client);
|
||||
|
||||
private readonly _pathToFileWizard = new ValidatedMap<string, InternalFileBreakpointsWizard>();
|
||||
|
||||
private constructor(
|
||||
private readonly _client: ExtendedDebugClient,
|
||||
private readonly _pausedWizard: PausedWizard,
|
||||
private readonly _project: TestProjectSpec,
|
||||
) {
|
||||
this._client.on('breakpoint', breakpointStatusChange =>
|
||||
this.onBreakpointStatusChange(breakpointStatusChange.body),
|
||||
);
|
||||
}
|
||||
|
||||
public get project() {
|
||||
return this._project;
|
||||
}
|
||||
|
||||
public static create(
|
||||
debugClient: ExtendedDebugClient,
|
||||
testProjectSpecification: TestProjectSpec,
|
||||
): BreakpointsWizard {
|
||||
return this.createWithPausedWizard(
|
||||
debugClient,
|
||||
PausedWizard.forClient(debugClient),
|
||||
testProjectSpecification,
|
||||
);
|
||||
}
|
||||
|
||||
public static createWithPausedWizard(
|
||||
debugClient: ExtendedDebugClient,
|
||||
pausedWizard: PausedWizard,
|
||||
testProjectSpecification: TestProjectSpec,
|
||||
): BreakpointsWizard {
|
||||
return wrapWithMethodLogger(new this(debugClient, pausedWizard, testProjectSpecification));
|
||||
}
|
||||
|
||||
public at(filePath: string): FileBreakpointsWizard {
|
||||
return wrapWithMethodLogger(
|
||||
new FileBreakpointsWizard(
|
||||
this._pathToFileWizard.getOrAdd(
|
||||
filePath,
|
||||
() =>
|
||||
new InternalFileBreakpointsWizard(
|
||||
wrapWithMethodLogger(this),
|
||||
this._client,
|
||||
this._project.src(filePath),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public async waitAndConsumePausedEvent(_breakpoint: BreakpointWizard): Promise<void> {
|
||||
// TODO: Should we validate the stack trace is on breakpoint here?
|
||||
await this._pausedWizard.waitAndConsumePausedEvent(pausedInfo => {
|
||||
expect(pausedInfo.reason).to.equal('breakpoint');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the debuggee to resume, and verify that the Debug-Adapter sends the proper notification after that happens
|
||||
*/
|
||||
public async resume(): Promise<void> {
|
||||
return this._pausedWizard.resume();
|
||||
}
|
||||
|
||||
public async waitAndConsumeResumedEvent(): Promise<void> {
|
||||
return this._pausedWizard.waitAndConsumeResumedEvent();
|
||||
}
|
||||
|
||||
public async waitAndAssertNoMoreEvents(): Promise<void> {
|
||||
return this._pausedWizard.waitAndAssertNoMoreEvents();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'Breakpoints';
|
||||
}
|
||||
|
||||
public async assertIsHitThenResumeWhen(
|
||||
breakpoints: BreakpointWizard[],
|
||||
lastActionToMakeBreakpointHit: () => Promise<void>,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
const actionResult = lastActionToMakeBreakpointHit();
|
||||
|
||||
for (const breakpoint of breakpoints) {
|
||||
await this.assertIsHitThenResume(breakpoint, verifications);
|
||||
}
|
||||
|
||||
await actionResult;
|
||||
}
|
||||
|
||||
public async assertIsHitThenResume(
|
||||
breakpoint: BreakpointWizard,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
await this.waitAndConsumePausedEvent(breakpoint);
|
||||
|
||||
const stackTraceFrames = (await stackTrace(this._client, verifications.stackFrameFormat))
|
||||
.stackFrames;
|
||||
|
||||
// Validate that the topFrame is locate in the same place as the breakpoint
|
||||
assertMatchesBreakpointLocation(stackTraceFrames[0], breakpoint.filePath, breakpoint);
|
||||
|
||||
if (typeof verifications.stackTrace === 'string') {
|
||||
const assertions = new StackTraceStringAssertions(breakpoint);
|
||||
assertions.assertResponseMatches(stackTraceFrames, verifications.stackTrace);
|
||||
} else if (typeof verifications.stackTrace === 'object') {
|
||||
const assertions = new StackTraceObjectAssertions(this);
|
||||
assertions.assertResponseMatches(stackTraceFrames, verifications.stackTrace);
|
||||
}
|
||||
|
||||
if (verifications.variables !== undefined) {
|
||||
await this._variablesWizard.assertStackFrameVariablesAre(
|
||||
new StackFrameWizard(this._client, stackTraceFrames[0]),
|
||||
verifications.variables,
|
||||
);
|
||||
}
|
||||
|
||||
if (verifications.action !== undefined) {
|
||||
await verifications.action();
|
||||
}
|
||||
|
||||
await this.resume();
|
||||
}
|
||||
|
||||
private onBreakpointStatusChange(
|
||||
breakpointStatusChanged: DebugProtocol.BreakpointEvent['body'],
|
||||
): void {
|
||||
if (this.isBreakpointStatusChangedWithId(breakpointStatusChanged)) {
|
||||
// TODO: Update this code to only send the breakpoint to the file that owns it
|
||||
for (const fileWizard of this._pathToFileWizard.values()) {
|
||||
fileWizard.onBreakpointStatusChange(breakpointStatusChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isBreakpointStatusChangedWithId(
|
||||
statusChanged: DebugProtocol.BreakpointEvent['body'],
|
||||
): statusChanged is BreakpointStatusChangedWithId {
|
||||
return statusChanged.breakpoint.id !== undefined;
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
import { BreakpointWizard } from './breakpointWizard';
|
||||
import { InternalFileBreakpointsWizard } from './implementation/internalFileBreakpointsWizard';
|
||||
import { PromiseOrNot } from '../../testUtils';
|
||||
import { wrapWithMethodLogger } from '../../core-v2/chrome/logging/methodsCalledLogger';
|
||||
import { PauseOnHitCount } from '../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
|
||||
|
||||
export interface IBreakpointOptions {
|
||||
text: string;
|
||||
boundText?: string;
|
||||
}
|
||||
|
||||
export interface IHitCountBreakpointOptions extends IBreakpointOptions {
|
||||
hitCountCondition: string;
|
||||
}
|
||||
|
||||
export interface IUnverifiedBreakpointOptions {
|
||||
text: string;
|
||||
unverifiedReason: string;
|
||||
}
|
||||
|
||||
export interface IUnverifiedHitCountBreakpointOptions extends IUnverifiedBreakpointOptions {
|
||||
hitCountCondition: string;
|
||||
}
|
||||
|
||||
export class FileBreakpointsWizard {
|
||||
public constructor(private readonly _internal: InternalFileBreakpointsWizard) {}
|
||||
|
||||
public async breakpoint(options: IBreakpointOptions): Promise<BreakpointWizard> {
|
||||
const wrappedBreakpoint = wrapWithMethodLogger(
|
||||
await this._internal.breakpoint({
|
||||
text: options.text,
|
||||
boundText: options.boundText,
|
||||
name: `BP @ ${options.text}`,
|
||||
}),
|
||||
);
|
||||
|
||||
return wrappedBreakpoint.setThenWaitForVerifiedThenValidate();
|
||||
}
|
||||
|
||||
public async hitCountBreakpoint(options: IHitCountBreakpointOptions): Promise<BreakpointWizard> {
|
||||
return await (await this.unsetHitCountBreakpoint(options)).setThenWaitForVerifiedThenValidate();
|
||||
}
|
||||
|
||||
public async unverifiedHitCountBreakpoint(
|
||||
options: IUnverifiedHitCountBreakpointOptions,
|
||||
): Promise<BreakpointWizard> {
|
||||
return (
|
||||
await (await this.unsetHitCountBreakpoint(options)).setWithoutVerifying()
|
||||
).assertIsNotVerified(options.unverifiedReason);
|
||||
}
|
||||
|
||||
public async unsetHitCountBreakpoint(
|
||||
options: IHitCountBreakpointOptions,
|
||||
): Promise<BreakpointWizard> {
|
||||
return wrapWithMethodLogger(
|
||||
await this._internal.breakpoint({
|
||||
text: options.text,
|
||||
boundText: options.boundText,
|
||||
actionWhenHit: new PauseOnHitCount(options.hitCountCondition),
|
||||
name: `BP @ ${options.text}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public batch<T>(
|
||||
batchAction: (fileBreakpointsWizard: FileBreakpointsWizard) => PromiseOrNot<T>,
|
||||
): Promise<T> {
|
||||
return this._internal.batch(batchAction);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `Breakpoints at ${this._internal.filePath}`;
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import _ = require('lodash');
|
||||
import { BreakpointWizard } from '../breakpointWizard';
|
||||
import { PromiseOrNot } from '../../../testUtils';
|
||||
import {
|
||||
IBreakpointsBatchingStrategy,
|
||||
InternalFileBreakpointsWizard,
|
||||
CurrentBreakpointsMapping,
|
||||
BreakpointsUpdate,
|
||||
BreakpointStatusChangedWithId,
|
||||
} from './internalFileBreakpointsWizard';
|
||||
import { IVerificationsAndAction } from '../breakpointsWizard';
|
||||
import { ValidatedSet } from '../../../core-v2/chrome/collections/validatedSet';
|
||||
|
||||
export class BatchingUpdatesState implements IBreakpointsBatchingStrategy {
|
||||
private readonly _breakpointsToSet = new ValidatedSet<BreakpointWizard>();
|
||||
private readonly _breakpointsToUnset = new ValidatedSet<BreakpointWizard>();
|
||||
private readonly _actionsToCompleteAfterBatch: (() => PromiseOrNot<void>)[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly _internal: InternalFileBreakpointsWizard,
|
||||
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping,
|
||||
) {}
|
||||
|
||||
public set(breakpointWizard: BreakpointWizard): void {
|
||||
this._breakpointsToSet.add(breakpointWizard);
|
||||
this._breakpointsToUnset.deleteIfExists(breakpointWizard);
|
||||
}
|
||||
|
||||
public unset(breakpointWizard: BreakpointWizard) {
|
||||
this._breakpointsToUnset.add(breakpointWizard);
|
||||
this._breakpointsToSet.deleteIfExists(breakpointWizard);
|
||||
}
|
||||
|
||||
public assertIsVerified(breakpoint: BreakpointWizard): void {
|
||||
this._actionsToCompleteAfterBatch.push(() => this._internal.assertIsVerified(breakpoint));
|
||||
}
|
||||
|
||||
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
|
||||
this._actionsToCompleteAfterBatch.push(() =>
|
||||
this._internal.assertIsNotVerified(breakpoint, unverifiedReason),
|
||||
);
|
||||
}
|
||||
|
||||
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
|
||||
this._actionsToCompleteAfterBatch.push(() => this._internal.waitUntilVerified(breakpoint));
|
||||
}
|
||||
|
||||
public onBreakpointStatusChange(_breakpointStatusChanged: BreakpointStatusChangedWithId): void {
|
||||
throw new Error(
|
||||
`Breakpoint status shouldn't be updated while doing a batch update. Is this happening due to a product or test bug?`,
|
||||
);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResumeWhen(
|
||||
_breakpoint: BreakpointWizard,
|
||||
_lastActionToMakeBreakpointHit: () => Promise<void>,
|
||||
_verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
throw new Error(
|
||||
`Breakpoint shouldn't be verified while doing a batch update. Is this happening due to a product or test bug?`,
|
||||
);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResume(
|
||||
_breakpoint: BreakpointWizard,
|
||||
_verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
throw new Error(
|
||||
`Breakpoint shouldn't be verified while doing a batch update. Is this happening due to a product or test bug?`,
|
||||
);
|
||||
}
|
||||
|
||||
public async processBatch(): Promise<void> {
|
||||
const breakpointsToKeepAsIs = _.difference(
|
||||
Array.from(this.currentBreakpointsMapping.keys()),
|
||||
this._breakpointsToSet.toArray(),
|
||||
this._breakpointsToUnset.toArray(),
|
||||
);
|
||||
|
||||
await this._internal.sendBreakpointsToClient(
|
||||
new BreakpointsUpdate(
|
||||
Array.from(this._breakpointsToSet),
|
||||
Array.from(this._breakpointsToUnset),
|
||||
breakpointsToKeepAsIs,
|
||||
),
|
||||
);
|
||||
|
||||
// this._internal.sendBreakpointsToClient changed the state to PerformChangesImmediatelyState so we can now execute the actions we had pending
|
||||
await this.executeActionsToCompleteAfterBatch();
|
||||
}
|
||||
|
||||
private async executeActionsToCompleteAfterBatch(): Promise<void> {
|
||||
// Validate with the originalSize that the actionsToCompleteAfterBatch aren't re-scheduled in a recursive way forever...
|
||||
const originalSize = this._actionsToCompleteAfterBatch.length;
|
||||
|
||||
for (const actionToComplete of this._actionsToCompleteAfterBatch) {
|
||||
await actionToComplete();
|
||||
}
|
||||
|
||||
if (this._actionsToCompleteAfterBatch.length > originalSize) {
|
||||
throw new Error(
|
||||
`The list of actions to complete increased while performing the actions to complete.` +
|
||||
` The actions to complete probably ended up recursively scheduling more actions which is a bug`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { expect, use } from 'chai';
|
||||
const chaiString = require('chai-string');
|
||||
import * as path from 'path';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { waitUntilReadyWithTimeout } from '../../../utils/waitUntilReadyWithTimeout';
|
||||
import { BreakpointWizard } from '../breakpointWizard';
|
||||
import {
|
||||
CurrentBreakpointsMapping,
|
||||
InternalFileBreakpointsWizard,
|
||||
} from './internalFileBreakpointsWizard';
|
||||
|
||||
use(chaiString);
|
||||
|
||||
interface IObjectWithLocation {
|
||||
source?: DebugProtocol.Source;
|
||||
line?: number; // One based line number
|
||||
column?: number; // One based colum number
|
||||
}
|
||||
|
||||
export class BreakpointsAssertions {
|
||||
public constructor(
|
||||
private readonly _internal: InternalFileBreakpointsWizard,
|
||||
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping,
|
||||
) {}
|
||||
|
||||
public assertIsVerified(breakpoint: BreakpointWizard): void {
|
||||
// Convert to one based to match the VS Code potocol and what VS Code does if you try to open that file at that line number
|
||||
|
||||
const breakpointStatus = this.currentBreakpointsMapping.get(breakpoint);
|
||||
assertMatchesBreakpointLocation(breakpointStatus, this._internal.filePath, breakpoint);
|
||||
expect(
|
||||
breakpointStatus.verified,
|
||||
`Expected ${breakpoint} to be verified yet it wasn't: ${breakpointStatus.message}`,
|
||||
).to.equal(true);
|
||||
}
|
||||
|
||||
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
|
||||
const breakpointLocation = `res:${breakpoint.filePath}:${breakpoint.position.lineNumber +
|
||||
1}:${breakpoint.position.columnNumber + 1}`;
|
||||
|
||||
// For the moment we are assuming that the breakpoint maps to a single script file. If we need to support other cases we'll need to compose the message in the proper way
|
||||
const fullMessage = `[ Breakpoint at ${breakpointLocation} do: ${breakpoint.actionWhenHit} is unbound because ${unverifiedReason} ]`;
|
||||
|
||||
const breakpointStatus = this.currentBreakpointsMapping.get(breakpoint);
|
||||
expect(
|
||||
breakpointStatus.verified,
|
||||
`Expected ${breakpoint} to not be verified yet it was: ${breakpointStatus.message}`,
|
||||
).to.equal(false);
|
||||
expect(
|
||||
breakpointStatus.message,
|
||||
`Expected ${breakpoint} to have a particular unverified message`,
|
||||
).to.equal(fullMessage);
|
||||
}
|
||||
|
||||
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
|
||||
await waitUntilReadyWithTimeout(() => this.currentBreakpointsMapping.get(breakpoint).verified);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMatchesBreakpointLocation(
|
||||
objectWithLocation: IObjectWithLocation,
|
||||
expectedFilePath: string,
|
||||
breakpoint: BreakpointWizard,
|
||||
): void {
|
||||
expect(objectWithLocation.source).to.not.equal(undefined);
|
||||
expect(objectWithLocation.source!.path!.toLowerCase()).to.be.equal(
|
||||
expectedFilePath.toLowerCase(),
|
||||
);
|
||||
expect(objectWithLocation.source!.name!.toLowerCase()).to.be.equal(
|
||||
path.basename(expectedFilePath.toLowerCase()),
|
||||
);
|
||||
|
||||
const expectedLineNumber = breakpoint.boundPosition.lineNumber + 1;
|
||||
const expectedColumNumber = breakpoint.boundPosition.columnNumber + 1;
|
||||
const expectedBPLocationPrinted = `${expectedFilePath}:${expectedLineNumber}:${expectedColumNumber}`;
|
||||
const actualBPLocationPrinted = `${objectWithLocation.source!.path}:${objectWithLocation.line}:${
|
||||
objectWithLocation.column
|
||||
}`;
|
||||
|
||||
expect(actualBPLocationPrinted.toLowerCase()).to.be.equal(
|
||||
expectedBPLocationPrinted.toLowerCase(),
|
||||
);
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import {
|
||||
BreakpointsUpdate,
|
||||
StateChanger,
|
||||
InternalFileBreakpointsWizard,
|
||||
BreakpointWithId,
|
||||
} from './internalFileBreakpointsWizard';
|
||||
import { BreakpointWizard, VSCodeActionWhenHit } from '../breakpointWizard';
|
||||
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
|
||||
import { BreakpointsWizard } from '../breakpointsWizard';
|
||||
import { Replace } from '../../../core-v2/typeUtils';
|
||||
import { ExtendedDebugClient } from '../../../testSupport/debugClient';
|
||||
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
|
||||
import {
|
||||
AlwaysPause,
|
||||
PauseOnHitCount,
|
||||
} from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
|
||||
|
||||
type SetBreakpointsResponseWithId = Replace<
|
||||
DebugProtocol.SetBreakpointsResponse,
|
||||
'body',
|
||||
Replace<DebugProtocol.SetBreakpointsResponse['body'], 'breakpoints', BreakpointWithId[]>
|
||||
>;
|
||||
|
||||
export class BreakpointsUpdater {
|
||||
public constructor(
|
||||
private readonly _breakpointsWizard: BreakpointsWizard,
|
||||
private readonly _internal: InternalFileBreakpointsWizard,
|
||||
private readonly _client: ExtendedDebugClient,
|
||||
private readonly _changeState: StateChanger,
|
||||
) {}
|
||||
|
||||
public async update(update: BreakpointsUpdate): Promise<void> {
|
||||
const updatedBreakpoints = update.toKeepAsIs.concat(update.toAdd);
|
||||
const vsCodeBps = updatedBreakpoints.map(bp => this.toVSCodeProtocol(bp));
|
||||
|
||||
const response = await this._client.setBreakpointsRequest({
|
||||
breakpoints: vsCodeBps,
|
||||
source: { path: this._internal.filePath },
|
||||
});
|
||||
|
||||
this.validateResponse(response, vsCodeBps);
|
||||
const responseWithIds = <SetBreakpointsResponseWithId>response;
|
||||
|
||||
const breakpointToStatus = new ValidatedMap<BreakpointWizard, BreakpointWithId>(
|
||||
<[[BreakpointWizard, BreakpointWithId]]>(
|
||||
_.zip(updatedBreakpoints, responseWithIds.body.breakpoints)
|
||||
),
|
||||
);
|
||||
this._changeState(
|
||||
new PerformChangesImmediatelyState(
|
||||
this._breakpointsWizard,
|
||||
this._internal,
|
||||
breakpointToStatus,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private toVSCodeProtocol(breakpoint: BreakpointWizard): DebugProtocol.SourceBreakpoint {
|
||||
// VS Code protocol is 1-based so we add one to the line and colum numbers
|
||||
const commonInformation = {
|
||||
line: breakpoint.position.lineNumber + 1,
|
||||
column: breakpoint.position.columnNumber + 1,
|
||||
};
|
||||
const actionWhenHitInformation = this.actionWhenHitToVSCodeProtocol(breakpoint);
|
||||
return Object.assign({}, commonInformation, actionWhenHitInformation);
|
||||
}
|
||||
|
||||
private actionWhenHitToVSCodeProtocol(breakpoint: BreakpointWizard): VSCodeActionWhenHit {
|
||||
if (breakpoint.actionWhenHit instanceof AlwaysPause) {
|
||||
return {};
|
||||
} else if (breakpoint.actionWhenHit instanceof PauseOnHitCount) {
|
||||
return { hitCondition: breakpoint.actionWhenHit.pauseOnHitCondition };
|
||||
} else {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
}
|
||||
|
||||
private validateResponse(
|
||||
response: DebugProtocol.SetBreakpointsResponse,
|
||||
vsCodeBps: DebugProtocol.SourceBreakpoint[],
|
||||
): void {
|
||||
if (!response.success) {
|
||||
throw new Error(`Failed to set the breakpoints for: ${this._internal.filePath}`);
|
||||
}
|
||||
|
||||
const expected = vsCodeBps.length;
|
||||
const actual = response.body.breakpoints.length;
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`Expected to receive ${expected} breakpoints yet we got ${actual}. Received breakpoints: ${JSON.stringify(
|
||||
response.body.breakpoints,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const bpsWithoutId = response.body.breakpoints.filter(bp => bp.id === undefined);
|
||||
if (bpsWithoutId.length !== 0) {
|
||||
throw new Error(
|
||||
`Expected to receive all breakpoints with id yet we got some without ${JSON.stringify(
|
||||
response.body.breakpoints,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { MakePropertyRequired, Replace } from '../../../core-v2/typeUtils';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { BreakpointWizard } from '../breakpointWizard';
|
||||
import { IVerificationsAndAction, BreakpointsWizard } from '../breakpointsWizard';
|
||||
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
|
||||
import { BreakpointsUpdater } from './breakpointsUpdater';
|
||||
import { PerformChangesImmediatelyState } from './performChangesImmediatelyState';
|
||||
import { ExtendedDebugClient } from '../../../testSupport/debugClient';
|
||||
import {
|
||||
IBPActionWhenHit,
|
||||
AlwaysPause,
|
||||
} from '../../../core-v2/chrome/internal/breakpoints/bpActionWhenHit';
|
||||
import { findPositionOfTextInFile } from '../../../utils/findPositionOfTextInFile';
|
||||
import { FileBreakpointsWizard } from '../fileBreakpointsWizard';
|
||||
import { PromiseOrNot } from '../../../testUtils';
|
||||
import { BatchingUpdatesState } from './batchingUpdatesState';
|
||||
|
||||
export type BreakpointWithId = MakePropertyRequired<DebugProtocol.Breakpoint, 'id'>;
|
||||
export type BreakpointStatusChangedWithId = Replace<
|
||||
DebugProtocol.BreakpointEvent['body'],
|
||||
'breakpoint',
|
||||
BreakpointWithId
|
||||
>;
|
||||
|
||||
export class BreakpointsUpdate {
|
||||
public constructor(
|
||||
public readonly toAdd: BreakpointWizard[],
|
||||
public readonly toRemove: BreakpointWizard[],
|
||||
public readonly toKeepAsIs: BreakpointWizard[],
|
||||
) {}
|
||||
}
|
||||
|
||||
export interface IBreakpointsBatchingStrategy {
|
||||
readonly currentBreakpointsMapping: CurrentBreakpointsMapping;
|
||||
|
||||
set(breakpointWizard: BreakpointWizard): void;
|
||||
unset(breakpointWizard: BreakpointWizard): void;
|
||||
|
||||
waitUntilVerified(breakpoint: BreakpointWizard): Promise<void>;
|
||||
assertIsVerified(breakpoint: BreakpointWizard): void;
|
||||
assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void;
|
||||
assertIsHitThenResumeWhen(
|
||||
breakpoint: BreakpointWizard,
|
||||
lastActionToMakeBreakpointHit: () => Promise<unknown>,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void>;
|
||||
assertIsHitThenResume(
|
||||
breakpoint: BreakpointWizard,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void>;
|
||||
|
||||
onBreakpointStatusChange(breakpointStatusChanged: BreakpointStatusChangedWithId): void;
|
||||
}
|
||||
|
||||
export type CurrentBreakpointsMapping = ValidatedMap<BreakpointWizard, BreakpointWithId>;
|
||||
|
||||
export type StateChanger = (newState: IBreakpointsBatchingStrategy) => void;
|
||||
|
||||
export class InternalFileBreakpointsWizard {
|
||||
private readonly _breakpointsUpdater = new BreakpointsUpdater(
|
||||
this._breakpointsWizard,
|
||||
this,
|
||||
this.client,
|
||||
state => (this._state = state),
|
||||
);
|
||||
|
||||
private _state: IBreakpointsBatchingStrategy = new PerformChangesImmediatelyState(
|
||||
this._breakpointsWizard,
|
||||
this,
|
||||
new ValidatedMap(),
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly _breakpointsWizard: BreakpointsWizard,
|
||||
public readonly client: ExtendedDebugClient,
|
||||
public readonly filePath: string,
|
||||
) {}
|
||||
|
||||
public async breakpoint(options: {
|
||||
name: string;
|
||||
text: string;
|
||||
boundText?: string;
|
||||
actionWhenHit?: IBPActionWhenHit;
|
||||
}) {
|
||||
const position = await findPositionOfTextInFile(this.filePath, options.text);
|
||||
const boundPosition = options.boundText
|
||||
? await findPositionOfTextInFile(this.filePath, options.boundText)
|
||||
: position;
|
||||
const actionWhenHit = options.actionWhenHit || new AlwaysPause();
|
||||
|
||||
return new BreakpointWizard(this, position, actionWhenHit, options.name, boundPosition);
|
||||
}
|
||||
|
||||
public async set(breakpointWizard: BreakpointWizard): Promise<void> {
|
||||
await this._state.set(breakpointWizard);
|
||||
}
|
||||
|
||||
public async unset(breakpointWizard: BreakpointWizard): Promise<void> {
|
||||
await this._state.unset(breakpointWizard);
|
||||
}
|
||||
|
||||
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
|
||||
await this._state.waitUntilVerified(breakpoint);
|
||||
}
|
||||
|
||||
public assertIsVerified(breakpoint: BreakpointWizard): void {
|
||||
this._state.assertIsVerified(breakpoint);
|
||||
}
|
||||
|
||||
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string) {
|
||||
this._state.assertIsNotVerified(breakpoint, unverifiedReason);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResumeWhen(
|
||||
breakpoint: BreakpointWizard,
|
||||
lastActionToMakeBreakpointHit: () => Promise<unknown>,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
return this._state.assertIsHitThenResumeWhen(
|
||||
breakpoint,
|
||||
lastActionToMakeBreakpointHit,
|
||||
verifications,
|
||||
);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResume(
|
||||
breakpoint: BreakpointWizard,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
return this._state.assertIsHitThenResume(breakpoint, verifications);
|
||||
}
|
||||
|
||||
public onBreakpointStatusChange(breakpointStatusChanged: BreakpointStatusChangedWithId): void {
|
||||
this._state.onBreakpointStatusChange(breakpointStatusChanged);
|
||||
}
|
||||
|
||||
public async batch<T>(
|
||||
batchAction: (fileBreakpointsWizard: FileBreakpointsWizard) => PromiseOrNot<T>,
|
||||
): Promise<T> {
|
||||
const batchingUpdates = new BatchingUpdatesState(this, this._state.currentBreakpointsMapping);
|
||||
this._state = batchingUpdates;
|
||||
const result = await batchAction(new FileBreakpointsWizard(this));
|
||||
await batchingUpdates.processBatch(); // processBatch calls sendBreakpointsToClient which will change the state back to PerformChangesImmediatelyState
|
||||
return result;
|
||||
}
|
||||
|
||||
public async sendBreakpointsToClient(update: BreakpointsUpdate): Promise<void> {
|
||||
return this._breakpointsUpdater.update(update);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { BreakpointWizard } from '../breakpointWizard';
|
||||
import {
|
||||
IBreakpointsBatchingStrategy,
|
||||
InternalFileBreakpointsWizard,
|
||||
CurrentBreakpointsMapping,
|
||||
BreakpointsUpdate,
|
||||
BreakpointStatusChangedWithId,
|
||||
} from './internalFileBreakpointsWizard';
|
||||
import { BreakpointsAssertions } from './breakpointsAssertions';
|
||||
import { BreakpointsWizard, IVerificationsAndAction } from '../breakpointsWizard';
|
||||
import { ValidatedMap } from '../../../core-v2/chrome/collections/validatedMap';
|
||||
|
||||
export class PerformChangesImmediatelyState implements IBreakpointsBatchingStrategy {
|
||||
private readonly _idToBreakpoint = new ValidatedMap<number, BreakpointWizard>();
|
||||
private readonly _breakpointsAssertions = new BreakpointsAssertions(
|
||||
this._internal,
|
||||
this.currentBreakpointsMapping,
|
||||
);
|
||||
|
||||
public constructor(
|
||||
private readonly _breakpointsWizard: BreakpointsWizard,
|
||||
private readonly _internal: InternalFileBreakpointsWizard,
|
||||
public readonly currentBreakpointsMapping: CurrentBreakpointsMapping,
|
||||
) {
|
||||
this.currentBreakpointsMapping.forEach((vsCodeStatus, breakpoint) => {
|
||||
this._idToBreakpoint.set(vsCodeStatus.id, breakpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public async set(breakpointWizard: BreakpointWizard): Promise<void> {
|
||||
if (this.currentBreakpointsMapping.has(breakpointWizard)) {
|
||||
throw new Error(`Can't set the breakpoint: ${breakpointWizard} because it's already set`);
|
||||
}
|
||||
|
||||
await this._internal.sendBreakpointsToClient(
|
||||
new BreakpointsUpdate([breakpointWizard], [], this.currentBreakpoints()),
|
||||
);
|
||||
}
|
||||
|
||||
public async unset(breakpointWizard: BreakpointWizard): Promise<void> {
|
||||
if (!this.currentBreakpointsMapping.has(breakpointWizard)) {
|
||||
throw new Error(`Can't unset the breakpoint: ${breakpointWizard} because it is not set`);
|
||||
}
|
||||
|
||||
const remainingBreakpoints = this.currentBreakpoints().filter(bp => bp !== breakpointWizard);
|
||||
await this._internal.sendBreakpointsToClient(
|
||||
new BreakpointsUpdate([], [breakpointWizard], remainingBreakpoints),
|
||||
);
|
||||
}
|
||||
|
||||
public onBreakpointStatusChange(breakpointStatusChanged: BreakpointStatusChangedWithId): void {
|
||||
const breakpoint = this._idToBreakpoint.get(breakpointStatusChanged.breakpoint.id);
|
||||
this.currentBreakpointsMapping.setAndReplaceIfExist(
|
||||
breakpoint,
|
||||
breakpointStatusChanged.breakpoint,
|
||||
);
|
||||
}
|
||||
|
||||
public assertIsVerified(breakpoint: BreakpointWizard): void {
|
||||
this._breakpointsAssertions.assertIsVerified(breakpoint);
|
||||
}
|
||||
|
||||
public assertIsNotVerified(breakpoint: BreakpointWizard, unverifiedReason: string): void {
|
||||
this._breakpointsAssertions.assertIsNotVerified(breakpoint, unverifiedReason);
|
||||
}
|
||||
|
||||
public async waitUntilVerified(breakpoint: BreakpointWizard): Promise<void> {
|
||||
await this._breakpointsAssertions.waitUntilVerified(breakpoint);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResumeWhen(
|
||||
breakpoint: BreakpointWizard,
|
||||
lastActionToMakeBreakpointHit: () => Promise<void>,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
await this._breakpointsWizard.assertIsHitThenResumeWhen(
|
||||
[breakpoint],
|
||||
lastActionToMakeBreakpointHit,
|
||||
verifications,
|
||||
);
|
||||
}
|
||||
|
||||
public async assertIsHitThenResume(
|
||||
breakpoint: BreakpointWizard,
|
||||
verifications: IVerificationsAndAction,
|
||||
): Promise<void> {
|
||||
await this._breakpointsWizard.assertIsHitThenResume(breakpoint, verifications);
|
||||
}
|
||||
|
||||
private currentBreakpoints(): BreakpointWizard[] {
|
||||
return Array.from(this.currentBreakpointsMapping.keys());
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
/** Remove the whitespaces from the start of each line and any comments we find at the end */
|
||||
export function trimWhitespaceAndComments(printedTestInput: string): string {
|
||||
return printedTestInput.replace(/^\s+/gm, '').replace(/ ?\/\/.*$/gm, ''); // Remove the white space we put at the start of the lines to make the printed test input align with the code
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { expect } from 'chai';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { BreakpointsWizard } from '../breakpointsWizard';
|
||||
import * as testSetup from '../../../testSetup';
|
||||
import { URL } from 'url';
|
||||
|
||||
export interface ExpectedSource {
|
||||
fileRelativePath?: string;
|
||||
url?: URL;
|
||||
evalCode?: boolean;
|
||||
}
|
||||
|
||||
export interface ExpectedFrame {
|
||||
name: string | RegExp;
|
||||
line?: number;
|
||||
column?: number;
|
||||
source?: ExpectedSource;
|
||||
presentationHint?: string;
|
||||
}
|
||||
|
||||
export class StackTraceObjectAssertions {
|
||||
private readonly _projectRoot: string;
|
||||
|
||||
public constructor(breakpointsWizard: BreakpointsWizard) {
|
||||
this._projectRoot = breakpointsWizard.project.props.projectRoot;
|
||||
}
|
||||
|
||||
private assertSourceMatches(
|
||||
actual: DebugProtocol.Source | undefined,
|
||||
expected: ExpectedSource | undefined,
|
||||
index: number,
|
||||
) {
|
||||
// tslint:disable-next-line: triple-equals
|
||||
if (actual == null && expected == null) {
|
||||
// TODO@rob
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: triple-equals
|
||||
if (expected == null) {
|
||||
// TODO@rob
|
||||
assert.fail(`Source was returned for frame ${index} but none was expected`);
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: triple-equals
|
||||
if (actual == null) {
|
||||
// TODO@rob
|
||||
assert.fail(`Source was expected for frame ${index} but none was returned`);
|
||||
return;
|
||||
}
|
||||
|
||||
let expectedName: string;
|
||||
let expectedPath: string;
|
||||
|
||||
if (expected.fileRelativePath) {
|
||||
// Generate the expected path from the relative path and the project root
|
||||
expectedPath = path.join(this._projectRoot, expected.fileRelativePath);
|
||||
expectedName = path.parse(expectedPath).base;
|
||||
} else if (expected.url) {
|
||||
expectedName = expected.url.host;
|
||||
expectedPath = expected.url.toString();
|
||||
} else if (expected.evalCode === true) {
|
||||
// Eval code has source that looks like 'VM123'. Check it by regex instead.
|
||||
expect(actual.name).to.match(/.*VM.*/, `Frame ${index} source name`);
|
||||
expect(actual.path).to.match(/.*VM.*/, `Frame ${index} source path`);
|
||||
return;
|
||||
} else {
|
||||
assert.fail(
|
||||
'Not enough information for expected source: set either "fileRelativePath" or "urlRelativePath" or "eval"',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(actual.name).to.equal(expectedName, `Frame ${index} source name`);
|
||||
expect(actual.path).to.equal(expectedPath, `Frame ${index} source path`);
|
||||
}
|
||||
|
||||
private assertFrameMatches(
|
||||
actual: DebugProtocol.StackFrame,
|
||||
expected: ExpectedFrame,
|
||||
index: number,
|
||||
) {
|
||||
if (typeof expected.name === 'string') {
|
||||
expect(actual.name).to.equal(expected.name, `Frame ${index} name`);
|
||||
} else if (expected.name instanceof RegExp) {
|
||||
expect(actual.name).to.match(expected.name, `Frame ${index} name`);
|
||||
}
|
||||
|
||||
expect(actual.line).to.equal(expected.line, `Frame ${index} line`);
|
||||
expect(actual.column).to.equal(expected.column, `Frame ${index} column`);
|
||||
|
||||
// Normal V1 stack frames will have no presentationHint, normal V2 stack frames will have presentationHint 'normal'
|
||||
if (testSetup.isThisV1 && expected.presentationHint === 'normal') {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
expect(actual.presentationHint, `Frame ${index} presentationHint`).to.be.undefined;
|
||||
} else {
|
||||
expect(actual.presentationHint).to.equal(
|
||||
expected.presentationHint,
|
||||
`Frame ${index} presentationHint`,
|
||||
);
|
||||
}
|
||||
|
||||
this.assertSourceMatches(actual.source, expected.source, index);
|
||||
}
|
||||
|
||||
private assertResponseMatchesFrames(
|
||||
actualFrames: DebugProtocol.StackFrame[],
|
||||
expectedFrames: ExpectedFrame[],
|
||||
) {
|
||||
// Check array length
|
||||
expect(actualFrames.length).to.equal(expectedFrames.length, 'Number of stack frames');
|
||||
|
||||
// Check each frame
|
||||
actualFrames.forEach((actualFrame, i) => {
|
||||
this.assertFrameMatches(actualFrame, expectedFrames[i], i);
|
||||
});
|
||||
}
|
||||
|
||||
public assertResponseMatches(
|
||||
stackTraceFrames: DebugProtocol.StackFrame[],
|
||||
expectedFrames: ExpectedFrame[],
|
||||
) {
|
||||
try {
|
||||
this.assertResponseMatchesFrames(stackTraceFrames, expectedFrames);
|
||||
} catch (e) {
|
||||
const error: assert.AssertionError = e;
|
||||
error.message +=
|
||||
'\nActual stack trace response: \n' + JSON.stringify(stackTraceFrames, null, 2);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { expect } from 'chai';
|
||||
import { BreakpointWizard } from '../breakpointWizard';
|
||||
import { trimWhitespaceAndComments } from './printedTestInputl';
|
||||
import { findLineNumber } from '../../../utils/findPositionOfTextInFile';
|
||||
|
||||
export class StackTraceStringAssertions {
|
||||
public constructor(private readonly _breakpoint: BreakpointWizard) {}
|
||||
|
||||
public assertResponseMatches(
|
||||
stackTraceFrames: DebugProtocol.StackFrame[],
|
||||
expectedString: string,
|
||||
) {
|
||||
stackTraceFrames.forEach(frame => {
|
||||
// Warning: We don't currently validate frame.source.path
|
||||
expect(frame.source).not.to.equal(undefined);
|
||||
const expectedSourceNameAndLine = ` [${frame.source!.name}] Line ${frame.line}`;
|
||||
(expect(
|
||||
frame.name,
|
||||
'Expected the formatted name to match the source name and line supplied as individual attributes',
|
||||
).to as any).endsWith(expectedSourceNameAndLine); // TODO@rob
|
||||
});
|
||||
|
||||
const formattedExpectedStackTrace = trimWhitespaceAndComments(expectedString);
|
||||
this.applyIgnores(formattedExpectedStackTrace, stackTraceFrames);
|
||||
const actualStackTrace = this.extractStackTrace(stackTraceFrames);
|
||||
assert.equal(
|
||||
actualStackTrace,
|
||||
formattedExpectedStackTrace,
|
||||
`Expected the stack trace when hitting ${this._breakpoint} to be:\n${formattedExpectedStackTrace}\nyet it is:\n${actualStackTrace}`,
|
||||
);
|
||||
}
|
||||
|
||||
private applyIgnores(
|
||||
formattedExpectedStackTrace: string,
|
||||
stackTrace: DebugProtocol.StackFrame[],
|
||||
): void {
|
||||
const ignoreFunctionNameText = '<__IGNORE_FUNCTION_NAME__>';
|
||||
const lineWithIgnoreIndex = formattedExpectedStackTrace.indexOf(ignoreFunctionNameText);
|
||||
if (lineWithIgnoreIndex >= 0) {
|
||||
const ignoreFunctionName = findLineNumber(formattedExpectedStackTrace, lineWithIgnoreIndex);
|
||||
expect(stackTrace.length).to.be.greaterThan(ignoreFunctionName);
|
||||
const ignoredFrame = stackTrace[ignoreFunctionName];
|
||||
ignoredFrame.name = `${ignoreFunctionNameText} [${ignoredFrame.source!.name}] Line ${
|
||||
ignoredFrame.line
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
private extractStackTrace(stackTrace: DebugProtocol.StackFrame[]): string {
|
||||
return stackTrace.map(f => this.printStackTraceFrame(f)).join('\n');
|
||||
}
|
||||
|
||||
private printStackTraceFrame(frame: DebugProtocol.StackFrame): string {
|
||||
const frameName = frame.name;
|
||||
return `${frameName}:${frame.column}${
|
||||
frame.presentationHint && frame.presentationHint !== 'normal'
|
||||
? ` (${frame.presentationHint})`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { expect } from 'chai';
|
||||
import { ValidatedMap } from '../core-v2/chrome/collections/validatedMap';
|
||||
import { ExtendedDebugClient, THREAD_ID } from '../testSupport/debugClient';
|
||||
import { wrapWithMethodLogger } from '../core-v2/chrome/logging/methodsCalledLogger';
|
||||
import { waitUntilReadyWithTimeout } from '../utils/waitUntilReadyWithTimeout';
|
||||
import { isThisV2 } from '../testSetup';
|
||||
import { promiseTimeout } from '../testUtils';
|
||||
|
||||
enum EventToConsume {
|
||||
Paused,
|
||||
Resumed,
|
||||
None,
|
||||
}
|
||||
|
||||
/** Helper methods to wait and/or verify when the debuggee was paused for any kind of pause.
|
||||
*
|
||||
* Warning: Needs to be created before the debuggee is launched to capture all events and avoid race conditions
|
||||
*/
|
||||
export class PausedWizard {
|
||||
private _noMoreEventsExpected = false;
|
||||
private _eventsToBeConsumed: (DebugProtocol.ContinuedEvent | DebugProtocol.StoppedEvent)[] = [];
|
||||
private static _clientToPausedWizard = new ValidatedMap<ExtendedDebugClient, PausedWizard>();
|
||||
|
||||
private constructor(private readonly _client: ExtendedDebugClient) {
|
||||
this._client.on('stopped', stopped => this.onEvent(stopped));
|
||||
this._client.on('continued', continued => this.onEvent(continued));
|
||||
}
|
||||
|
||||
private onEvent(continued: any) {
|
||||
this.validateNoMoreEventsIfSet(continued);
|
||||
this._eventsToBeConsumed.push(continued);
|
||||
this.logState();
|
||||
}
|
||||
|
||||
// The PausedWizard logic will break if we create 2 PausedWizards for the same client. So we warranty we only create one
|
||||
public static forClient(client: ExtendedDebugClient): PausedWizard {
|
||||
return this._clientToPausedWizard.getOrAdd(client, () =>
|
||||
wrapWithMethodLogger(new PausedWizard(client)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the debuggee is not paused
|
||||
*
|
||||
* @param millisecondsToWaitForPauses How much time to wait for pauses
|
||||
*/
|
||||
public async waitAndConsumeResumedEvent(): Promise<void> {
|
||||
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Resumed);
|
||||
this.markNextEventAsConsumed('continued');
|
||||
}
|
||||
|
||||
/** Return whether the debuggee is currently paused */
|
||||
public isPaused(): boolean {
|
||||
return this.nextEventToConsume === EventToConsume.Paused;
|
||||
}
|
||||
|
||||
/** Wait and block until the debuggee is paused on a debugger statement */
|
||||
public async waitUntilPausedOnDebuggerStatement(): Promise<void> {
|
||||
return this.waitAndConsumePausedEvent(pauseInfo => {
|
||||
expect(pauseInfo.description).to.equal('Paused on debugger statement');
|
||||
expect(pauseInfo.reason).to.equal('debugger_statement');
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait and block until the debuggee is paused, and then perform the specified action with the pause event's body */
|
||||
public async waitAndConsumePausedEvent(
|
||||
actionWithPausedInfo: (pausedInfo: DebugProtocol.StoppedEvent['body']) => void,
|
||||
): Promise<void> {
|
||||
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Paused);
|
||||
const pausedEvent = <DebugProtocol.StoppedEvent>this._eventsToBeConsumed[0];
|
||||
this.markNextEventAsConsumed('stopped');
|
||||
await actionWithPausedInfo(pausedEvent.body);
|
||||
}
|
||||
|
||||
/** Wait and block until the debuggee has been resumed */
|
||||
public async waitUntilResumed(): Promise<void> {
|
||||
// We assume that nobody is consuming events in parallel, so if we start paused, the wait call won't ever succeed
|
||||
expect(this.nextEventToConsume).to.not.equal(EventToConsume.Paused);
|
||||
|
||||
await waitUntilReadyWithTimeout(() => this.nextEventToConsume === EventToConsume.Resumed);
|
||||
|
||||
this.markNextEventAsConsumed('continued');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the debuggee to resume, and verify that the Debug-Adapter sends the proper notification after that happens
|
||||
*/
|
||||
public async resume(): Promise<void> {
|
||||
await this._client.continueRequest();
|
||||
if (isThisV2) {
|
||||
// TODO: Is getting this event on V2 a bug? See: Continued Event at https://microsoft.github.io/debug-adapter-protocol/specification
|
||||
await this.waitUntilResumed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruct the debuggee to pause, and verify that the Debug-Adapter sends the proper notification after that happens
|
||||
*/
|
||||
public async pause(): Promise<void> {
|
||||
await this._client.pauseRequest({ threadId: THREAD_ID });
|
||||
|
||||
await this.waitAndConsumePausedEvent(event => {
|
||||
expect(event.reason).to.equal('pause');
|
||||
expect(event.description).to.equal('Paused on user request');
|
||||
});
|
||||
}
|
||||
|
||||
public async waitAndAssertNoMoreEvents(): Promise<void> {
|
||||
expect(this.nextEventToConsume).to.equal(EventToConsume.None);
|
||||
this._noMoreEventsExpected = true;
|
||||
|
||||
// Wait some time, to see if any events appear eventually
|
||||
await promiseTimeout(undefined, 500);
|
||||
|
||||
expect(this.nextEventToConsume).to.equal(EventToConsume.None);
|
||||
}
|
||||
|
||||
private validateNoMoreEventsIfSet(
|
||||
event: DebugProtocol.ContinuedEvent | DebugProtocol.StoppedEvent,
|
||||
): void {
|
||||
if (this._noMoreEventsExpected && event.event !== 'continued') {
|
||||
// TODO@rob
|
||||
throw new Error(
|
||||
`Received an event after it was signaled that no more events were expected: ${JSON.stringify(
|
||||
event,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private logState() {
|
||||
// logger.log(`Resume/Pause #events = ${this._eventsToBeConsumed.length}, state = ${EventToConsume[this.nextEventToConsume]}`); // TODO@rob
|
||||
}
|
||||
|
||||
private get nextEventToConsume(): EventToConsume {
|
||||
if (this._eventsToBeConsumed.length === 0) {
|
||||
return EventToConsume.None;
|
||||
} else {
|
||||
const nextEventToBeConsumed = this._eventsToBeConsumed[0];
|
||||
switch (nextEventToBeConsumed.event) {
|
||||
case 'stopped':
|
||||
return EventToConsume.Paused;
|
||||
case 'continued':
|
||||
return EventToConsume.Resumed;
|
||||
default:
|
||||
throw new Error(
|
||||
`Expected the event to be consumed to be either a stopped or continued yet it was: ${JSON.stringify(
|
||||
nextEventToBeConsumed,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private markNextEventAsConsumed(eventName: 'continued' | 'stopped'): void {
|
||||
expect(this._eventsToBeConsumed).length.to.be.greaterThan(0);
|
||||
expect(this._eventsToBeConsumed[0].event).to.equal(eventName);
|
||||
this._eventsToBeConsumed.shift();
|
||||
this.logState();
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return 'PausedWizard';
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
export type ManyVariablesPropertiesPrinted = string; // `${variable.name} = ${variable.value} ${(variable.type)}\n`
|
|
@ -1,143 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { VariablesScopeName } from './variablesWizard';
|
||||
import { ValidatedSet, IValidatedSet } from '../../core-v2/chrome/collections/validatedSet';
|
||||
import { singleElementOfArray } from '../../core-v2/chrome/collections/utilities';
|
||||
import { ExtendedDebugClient, THREAD_ID } from '../../testSupport/debugClient';
|
||||
|
||||
interface IVariablesOfScope {
|
||||
scopeName: VariablesScopeName;
|
||||
variables: DebugProtocol.Variable[];
|
||||
}
|
||||
|
||||
const defaultStackFrameFormat: DebugProtocol.StackFrameFormat = {
|
||||
parameters: true,
|
||||
parameterTypes: true,
|
||||
parameterNames: true,
|
||||
line: true,
|
||||
module: true,
|
||||
};
|
||||
|
||||
export async function stackTrace(
|
||||
client: ExtendedDebugClient,
|
||||
optionalStackFrameFormat?: DebugProtocol.StackFrameFormat,
|
||||
): Promise<DebugProtocol.StackTraceResponse['body']> {
|
||||
const stackFrameFormat = _.defaultTo(optionalStackFrameFormat, defaultStackFrameFormat);
|
||||
const stackTraceResponse = await client.send('stackTrace', {
|
||||
threadId: THREAD_ID,
|
||||
format: stackFrameFormat,
|
||||
});
|
||||
expect(
|
||||
stackTraceResponse.success,
|
||||
`Expected the response to the stack trace request to be succesful yet it failed: ${JSON.stringify(
|
||||
stackTraceResponse,
|
||||
)}`,
|
||||
).to.equal(true);
|
||||
|
||||
// Check totalFrames property
|
||||
expect(stackTraceResponse.body.totalFrames).to.equal(
|
||||
stackTraceResponse.body.stackFrames.length,
|
||||
'body.totalFrames',
|
||||
);
|
||||
return stackTraceResponse.body;
|
||||
}
|
||||
|
||||
export async function topStackFrame(
|
||||
client: ExtendedDebugClient,
|
||||
optionalStackFrameFormat?: DebugProtocol.StackFrameFormat,
|
||||
): Promise<DebugProtocol.StackFrame> {
|
||||
const stackFrames = (await stackTrace(client, optionalStackFrameFormat)).stackFrames;
|
||||
expect(stackFrames.length).to.be.greaterThan(0);
|
||||
return stackFrames[0];
|
||||
}
|
||||
|
||||
/** Utility functions to operate on the stack straces and stack frames of the debuggee.
|
||||
* It also provides utilities to access the scopes available in a particular stack frame.
|
||||
*/
|
||||
export class StackFrameWizard {
|
||||
public constructor(
|
||||
private readonly _client: ExtendedDebugClient,
|
||||
private readonly _stackFrame: DebugProtocol.StackFrame,
|
||||
) {}
|
||||
|
||||
/** Return a Wizard to interact with the top stack frame of the debuggee of the client */
|
||||
public static async topStackFrame(client: ExtendedDebugClient): Promise<StackFrameWizard> {
|
||||
return new StackFrameWizard(client, await topStackFrame(client));
|
||||
}
|
||||
|
||||
/** Return the variables information for the scopes selected by name */
|
||||
public async variablesOfScopes(
|
||||
manyScopeNames: VariablesScopeName[],
|
||||
): Promise<IVariablesOfScope[]> {
|
||||
const scopes = await this.scopesByNames(manyScopeNames);
|
||||
return Promise.all(
|
||||
scopes.map(async scope => {
|
||||
const variablesResponse = await this._client.variablesRequest({
|
||||
variablesReference: scope!.variablesReference,
|
||||
});
|
||||
expect(variablesResponse.success).to.equal(true);
|
||||
expect(variablesResponse.body).not.to.equal(undefined);
|
||||
const variables = variablesResponse.body.variables;
|
||||
expect(variables).not.to.equal(undefined);
|
||||
return { scopeName: <VariablesScopeName>scope.name.toLowerCase(), variables };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async scopesByNames(
|
||||
manyScopeNames: VariablesScopeName[],
|
||||
): Promise<DebugProtocol.Scope[]> {
|
||||
const scopeNamesSet = new ValidatedSet(manyScopeNames.map(name => name.toLowerCase()));
|
||||
const requestedScopes = (await this.scopes()).filter(scope =>
|
||||
scopeNamesSet.has(scope.name.toLowerCase()),
|
||||
);
|
||||
expect(requestedScopes).to.have.lengthOf(manyScopeNames.length);
|
||||
return requestedScopes;
|
||||
}
|
||||
|
||||
/** Return all the scopes available in the underlying stack frame */
|
||||
public async scopes(): Promise<DebugProtocol.Scope[]> {
|
||||
const scopesResponse = await this._client.scopesRequest({ frameId: this._stackFrame.id });
|
||||
// logger.log(`Scopes: ${scopesResponse.body.scopes.map(s => s.name).join(', ')}`); // TODO@rob
|
||||
return scopesResponse.body.scopes;
|
||||
}
|
||||
|
||||
/** Return the names of all the global variables in the underlying stack frame */
|
||||
public async globalVariableNames(): Promise<IValidatedSet<string>> {
|
||||
const existingGlobalVariables = await this.variablesOfScope('global');
|
||||
return new ValidatedSet(existingGlobalVariables.map(variable => variable.name));
|
||||
}
|
||||
|
||||
/** Return the variables information for a particular scope of the underlying stack frame */
|
||||
public async variablesOfScope(scopeName: VariablesScopeName): Promise<DebugProtocol.Variable[]> {
|
||||
return singleElementOfArray(await this.variablesOfScopes([scopeName])).variables;
|
||||
}
|
||||
|
||||
public async variable(
|
||||
variableName: string,
|
||||
): Promise<{ scope: DebugProtocol.Scope; variable: DebugProtocol.Variable }> {
|
||||
const scopes = await this.scopes();
|
||||
for (const scope of scopes) {
|
||||
const variablesResponse = await this._client.variablesRequest({
|
||||
variablesReference: scope!.variablesReference,
|
||||
});
|
||||
expect(variablesResponse.success).to.equal(true);
|
||||
expect(variablesResponse.body).not.to.equal(undefined);
|
||||
const variables = variablesResponse.body.variables;
|
||||
expect(variables).not.to.equal(undefined);
|
||||
const variable = variables.find(eachVariable => eachVariable.name === variableName);
|
||||
if (variable !== undefined) {
|
||||
return { scope, variable };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`A variable named ${variableName} wasn't found in any of the scopes of ${this}`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
export interface IVariableInformation {
|
||||
name: string;
|
||||
value: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a collection of variable informations to make it easier to compare
|
||||
* the expected variables of a test, and the actual variables of the debuggee
|
||||
*/
|
||||
export function printVariables(variables: IVariableInformation[]): string {
|
||||
const variablesPrinted = variables.map(variable => printVariable(variable));
|
||||
return variablesPrinted.join('\n');
|
||||
}
|
||||
|
||||
function printVariable(variable: IVariableInformation): string {
|
||||
return `${variable.name} = ${variable.value} (${variable.type})`;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { expect } from 'chai';
|
||||
import { DebugProtocol } from 'vscode-debugprotocol';
|
||||
import { trimWhitespaceAndComments } from '../breakpoints/implementation/printedTestInputl';
|
||||
import { ManyVariablesValues } from './variablesWizard';
|
||||
import { printVariables } from './variablesPrinting';
|
||||
|
||||
/** Whether the expected variables should match exactly the actual variables of the debuggee
|
||||
* or whether the expected variables should only be a subset of the actual variables of the debuggee
|
||||
*/
|
||||
export enum KindOfVerification {
|
||||
SameAndExact /** Same exact variables */,
|
||||
ProperSubset /** Expected variables are a subset of the actual variables */,
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide methods to validate that the variables appearing on the stack trace are what we expect
|
||||
*/
|
||||
export class VariablesVerifier {
|
||||
/** Verify that the actual variables are exactly the variables that we expect */
|
||||
public assertVariablesAre(
|
||||
variables: DebugProtocol.Variable[],
|
||||
expectedVariables: string | ManyVariablesValues,
|
||||
): void {
|
||||
if (typeof expectedVariables === 'string') {
|
||||
this.assertVariablesPrintedAre(variables, expectedVariables);
|
||||
} else {
|
||||
this.assertVariablesValuesAre(variables, expectedVariables);
|
||||
}
|
||||
}
|
||||
|
||||
private assertVariablesPrintedAre(
|
||||
variables: DebugProtocol.Variable[],
|
||||
expectedVariablesPrinted: string,
|
||||
): void {
|
||||
const trimmedVariables = trimWhitespaceAndComments(expectedVariablesPrinted);
|
||||
expect(printVariables(variables)).to.equal(trimmedVariables);
|
||||
}
|
||||
|
||||
private assertVariablesValuesAre(
|
||||
manyVariables: DebugProtocol.Variable[],
|
||||
expectedVariablesValues: ManyVariablesValues,
|
||||
): void {
|
||||
return this.assertVariablesValuesSatisfy(
|
||||
manyVariables,
|
||||
expectedVariablesValues,
|
||||
KindOfVerification.SameAndExact,
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify that the actual variables include as a proper subset the variables that we expect */
|
||||
public assertVariablesValuesContain(
|
||||
manyVariables: DebugProtocol.Variable[],
|
||||
expectedVariablesValues: ManyVariablesValues,
|
||||
): void {
|
||||
return this.assertVariablesValuesSatisfy(
|
||||
manyVariables,
|
||||
expectedVariablesValues,
|
||||
KindOfVerification.ProperSubset,
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify that the actual variables match the expected variables with the verification specified as a parameter (Same or subset) */
|
||||
public assertVariablesValuesSatisfy(
|
||||
manyVariables: DebugProtocol.Variable[],
|
||||
expectedVariablesValues: ManyVariablesValues,
|
||||
kindOfVerification: KindOfVerification,
|
||||
): void {
|
||||
const actualVariableNames = manyVariables.map(variable => variable.name);
|
||||
const expectedVariablesNames = Object.keys(expectedVariablesValues);
|
||||
switch (kindOfVerification) {
|
||||
case KindOfVerification.ProperSubset:
|
||||
expect(actualVariableNames).to.contain.members(expectedVariablesNames);
|
||||
break;
|
||||
case KindOfVerification.SameAndExact:
|
||||
expect(actualVariableNames).to.have.members(expectedVariablesNames);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected comparison algorithm: ${kindOfVerification}`);
|
||||
}
|
||||
|
||||
for (const variable of manyVariables) {
|
||||
const variableName = variable.name;
|
||||
if (expectedVariablesNames.indexOf(variableName) >= 0) {
|
||||
const expectedValue = expectedVariablesValues[variableName];
|
||||
expect(expectedValue).to.not.equal(undefined);
|
||||
expect(variable!.evaluateName).to.be.equal(variable!.name); // Is this ever different?
|
||||
expect(variable!.variablesReference).to.be.greaterThan(-1);
|
||||
expect(variable!.value).to.be.equal(`${expectedValue}`);
|
||||
// TODO: Validate variable type too
|
||||
} else {
|
||||
expect(kindOfVerification).to.equal(KindOfVerification.ProperSubset); // This should not happen for same elements
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { PromiseOrNot } from '../../testUtils';
|
||||
import { StackFrameWizard } from './stackFrameWizard';
|
||||
import { VariablesVerifier } from './variablesVerifier';
|
||||
import { ValidatedMap } from '../../core-v2/chrome/collections/validatedMap';
|
||||
import { trimWhitespaceAndComments } from '../breakpoints/implementation/printedTestInputl';
|
||||
import { expect } from 'chai';
|
||||
import { printVariables } from './variablesPrinting';
|
||||
import { ExtendedDebugClient } from '../../testSupport/debugClient';
|
||||
|
||||
export interface VariablePrintedProperties {
|
||||
value: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ManyVariablePrintedProperties {
|
||||
[variableName: string]: VariablePrintedProperties;
|
||||
}
|
||||
|
||||
export interface ManyVariablesValues {
|
||||
[variableName: string]: unknown;
|
||||
}
|
||||
|
||||
export type ManyVariablesPropertiesPrinted = string; // `${variable.name} = ${variable.value} ${(variable.type)}\n`
|
||||
|
||||
export type IScopeExpectedVariables = ManyVariablesPropertiesPrinted | ManyVariablesValues;
|
||||
|
||||
export interface IExpectedVariables {
|
||||
script?: IScopeExpectedVariables;
|
||||
local?: IScopeExpectedVariables;
|
||||
global?: IScopeExpectedVariables;
|
||||
catch?: IScopeExpectedVariables;
|
||||
block?: IScopeExpectedVariables;
|
||||
closure?: IScopeExpectedVariables;
|
||||
eval?: IScopeExpectedVariables;
|
||||
with?: IScopeExpectedVariables;
|
||||
module?: IScopeExpectedVariables;
|
||||
|
||||
local_contains?: ManyVariablesValues;
|
||||
}
|
||||
|
||||
export type VariablesScopeName = keyof IExpectedVariables;
|
||||
export type VerificationModifier = 'contains' | '';
|
||||
|
||||
export class VariablesWizard {
|
||||
public constructor(private readonly _client: ExtendedDebugClient) {}
|
||||
|
||||
/** Verify that the global variables have the expected values, ignoring the variables in <namesOfGlobalsToIgnore> */
|
||||
public async assertNewGlobalVariariablesAre(
|
||||
actionThatAddsNewVariables: () => PromiseOrNot<void>,
|
||||
expectedGlobals: ManyVariablesPropertiesPrinted,
|
||||
): Promise<void> {
|
||||
// Store pre-existing global variables' names
|
||||
const namesOfGlobalsToIgnore = await (await this.topStackFrameHelper()).globalVariableNames();
|
||||
|
||||
// Perform an action that adds new global variables
|
||||
await actionThatAddsNewVariables();
|
||||
|
||||
const globalsOnFrame = await (await this.topStackFrameHelper()).variablesOfScope('global');
|
||||
const nonIgnoredGlobals = globalsOnFrame.filter(
|
||||
global => !namesOfGlobalsToIgnore.has(global.name),
|
||||
);
|
||||
const expectedGlobalsTrimmed = trimWhitespaceAndComments(expectedGlobals);
|
||||
expect(printVariables(nonIgnoredGlobals)).to.equal(expectedGlobalsTrimmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the stackFrame contains some variables with a specific value
|
||||
*/
|
||||
public async assertTopFrameVariablesAre(verifications: IExpectedVariables): Promise<void> {
|
||||
await this.assertStackFrameVariablesAre(await this.topStackFrameHelper(), verifications);
|
||||
}
|
||||
|
||||
public async assertStackFrameVariablesAre(
|
||||
stackFrame: StackFrameWizard,
|
||||
verifications: IExpectedVariables,
|
||||
) {
|
||||
const scopesWithModifiers = Object.keys(verifications);
|
||||
const scopesWithoutModifiers = scopesWithModifiers.map(
|
||||
s => this.splitIntoScopeNameAndModifier(s)[0],
|
||||
);
|
||||
const zippedScopes = _.zip(scopesWithoutModifiers, scopesWithModifiers) as [
|
||||
keyof IExpectedVariables,
|
||||
keyof IExpectedVariables,
|
||||
][];
|
||||
const withoutModifiersToWith = new ValidatedMap<
|
||||
keyof IExpectedVariables,
|
||||
keyof IExpectedVariables
|
||||
>(zippedScopes);
|
||||
const manyScopes = await stackFrame.variablesOfScopes(scopesWithoutModifiers);
|
||||
for (const scope of manyScopes) {
|
||||
const scopeNameWithModifier = withoutModifiersToWith.get(scope.scopeName)!;
|
||||
const [, modifier] = this.splitIntoScopeNameAndModifier(scopeNameWithModifier);
|
||||
switch (modifier) {
|
||||
case '':
|
||||
this.verificator.assertVariablesAre(
|
||||
scope.variables,
|
||||
verifications[scopeNameWithModifier] as IScopeExpectedVariables,
|
||||
);
|
||||
break;
|
||||
case 'contains':
|
||||
this.verificator.assertVariablesValuesContain(
|
||||
scope.variables,
|
||||
<ManyVariablesValues>verifications[scopeNameWithModifier]!,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown modified used for variables verification: ${modifier} in ${scopeNameWithModifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async set(variableName: string, newValue: string): Promise<void> {
|
||||
const stackFrame = await this.topStackFrameHelper();
|
||||
const { scope, variable } = await stackFrame.variable(variableName);
|
||||
const response = await this._client.setVariableRequest({
|
||||
variablesReference: scope.variablesReference,
|
||||
name: variable.name,
|
||||
value: newValue,
|
||||
});
|
||||
expect(response.success).to.equal(true);
|
||||
}
|
||||
|
||||
private splitIntoScopeNameAndModifier(
|
||||
modifiedScopeName: keyof IExpectedVariables,
|
||||
): [VariablesScopeName, VerificationModifier] {
|
||||
const components = modifiedScopeName.split('_');
|
||||
if (components.length > 2) {
|
||||
throw new Error(`Invalid modified scope name: ${modifiedScopeName}`);
|
||||
}
|
||||
|
||||
return [
|
||||
<VariablesScopeName>components[0],
|
||||
<VerificationModifier>_.defaultTo(components[1], ''),
|
||||
];
|
||||
}
|
||||
|
||||
private get verificator(): VariablesVerifier {
|
||||
return new VariablesVerifier();
|
||||
}
|
||||
|
||||
private async topStackFrameHelper(): Promise<StackFrameWizard> {
|
||||
return await StackFrameWizard.topStackFrame(this._client);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/script.js:9:1
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 2
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 1
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 3
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 4
|
|
@ -0,0 +1,8 @@
|
|||
stderr> Invalid hit condition "abc". Expected an expression like "> 42" or "== 2".
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 0
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:2:3
|
||||
result: 1
|
||||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on debugger statement
|
||||
reason : pause
|
||||
threadId : <number>
|
||||
}
|
||||
<anonymous> @ ${workspaceFolder}/web/condition.js:5:1
|
|
@ -407,6 +407,122 @@ describe('breakpoints', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hit condition', () => {
|
||||
async function waitForPauseAndLogI(p: ITestHandle) {
|
||||
await waitForPause(p, async () => {
|
||||
await p.logger.evaluateAndLog('i');
|
||||
});
|
||||
}
|
||||
|
||||
itIntegrates('exact', async ({ r }) => {
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, hitCondition: '==2' }],
|
||||
});
|
||||
p.load();
|
||||
await waitForPauseAndLogI(p);
|
||||
await waitForPause(p);
|
||||
p.assertLog();
|
||||
});
|
||||
|
||||
itIntegrates('less than', async ({ r }) => {
|
||||
// Breakpoint in separate script set before launch.
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, hitCondition: '<3' }],
|
||||
});
|
||||
p.load();
|
||||
|
||||
await waitForPauseAndLogI(p);
|
||||
await waitForPauseAndLogI(p);
|
||||
await waitForPause(p);
|
||||
p.assertLog();
|
||||
});
|
||||
|
||||
itIntegrates('greater than', async ({ r }) => {
|
||||
// Breakpoint in separate script set before launch.
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, hitCondition: '>3' }],
|
||||
});
|
||||
p.load();
|
||||
|
||||
await waitForPauseAndLogI(p);
|
||||
await waitForPauseAndLogI(p);
|
||||
p.assertLog();
|
||||
});
|
||||
|
||||
itIntegrates('invalid', async ({ r }) => {
|
||||
// Breakpoint in separate script set before launch.
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
p.dap.on('output', output => {
|
||||
if (output.category === 'stderr') {
|
||||
p.logger.logOutput(output);
|
||||
}
|
||||
});
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, hitCondition: 'abc' }],
|
||||
});
|
||||
p.load();
|
||||
await waitForPause(p); // falls through to debugger statement
|
||||
p.assertLog();
|
||||
});
|
||||
});
|
||||
|
||||
describe('condition', () => {
|
||||
async function waitForPauseAndLogI(p: ITestHandle) {
|
||||
await waitForPause(p, async () => {
|
||||
await p.logger.evaluateAndLog('i');
|
||||
});
|
||||
}
|
||||
|
||||
itIntegrates('basic', async ({ r }) => {
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, condition: 'i==2' }],
|
||||
});
|
||||
p.load();
|
||||
await waitForPauseAndLogI(p);
|
||||
await waitForPause(p);
|
||||
p.assertLog();
|
||||
});
|
||||
|
||||
itIntegrates('ignores bp with invalid condition', async ({ r }) => {
|
||||
// Breakpoint in separate script set before launch.
|
||||
const p = await r.launchUrl('condition.html');
|
||||
const source: Dap.Source = {
|
||||
path: p.workspacePath('web/condition.js'),
|
||||
};
|
||||
await p.dap.setBreakpoints({
|
||||
source,
|
||||
breakpoints: [{ line: 2, column: 0, condition: ')(}{][.&' }],
|
||||
});
|
||||
p.load();
|
||||
await waitForPause(p); // falls through to debugger statement
|
||||
p.assertLog();
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom', () => {
|
||||
itIntegrates('inner html', async ({ r }) => {
|
||||
// Custom breakpoint for innerHtml.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
allThreadsStopped : false
|
||||
description : Paused on breakpoint
|
||||
reason : breakpoint
|
||||
threadId : <number>
|
||||
}
|
||||
App @ ${fixturesDir}/react-test/src/App.tsx:6:1
|
|
@ -0,0 +1,103 @@
|
|||
/*---------------------------------------------------------
|
||||
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||
*--------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { getDeferred } from '../../common/promiseUtil';
|
||||
import Dap from '../../dap/api';
|
||||
import { killTree } from '../../targets/node/killTree';
|
||||
import { ITestHandle, testFixturesDir } from '../test';
|
||||
import { itIntegrates } from '../testIntegrationUtils';
|
||||
|
||||
describe('react', () => {
|
||||
async function waitForPause(p: ITestHandle, cb?: (threadId: string) => Promise<void>) {
|
||||
const { threadId } = p.log(await p.dap.once('stopped'));
|
||||
await p.logger.logStackTrace(threadId);
|
||||
if (cb) await cb(threadId);
|
||||
return p.dap.continue({ threadId });
|
||||
}
|
||||
|
||||
const projectName = 'react-test';
|
||||
let projectFolder: string;
|
||||
let devServerProc: cp.ChildProcessWithoutNullStreams | undefined;
|
||||
beforeEach(async function() {
|
||||
this.timeout(60000 * 4);
|
||||
projectFolder = join(testFixturesDir, projectName);
|
||||
await setupCRA(projectName, testFixturesDir);
|
||||
devServerProc = await startDevServer(projectFolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (devServerProc) {
|
||||
console.log('Killing ' + devServerProc.pid);
|
||||
killTree(devServerProc.pid);
|
||||
}
|
||||
});
|
||||
|
||||
itIntegrates('hit breakpoint', async ({ r }) => {
|
||||
// Breakpoint in inline script set before launch.
|
||||
const p = await r._launch('http://localhost:3000', {
|
||||
webRoot: projectFolder,
|
||||
__workspaceFolder: projectFolder,
|
||||
rootPath: projectFolder,
|
||||
});
|
||||
const source: Dap.Source = {
|
||||
path: join(projectFolder, 'src/App.tsx'),
|
||||
};
|
||||
await p.dap.setBreakpoints({ source, breakpoints: [{ line: 6, column: 0 }] });
|
||||
p.load();
|
||||
await waitForPause(p);
|
||||
p.assertLog({ substring: true });
|
||||
});
|
||||
});
|
||||
|
||||
async function setupCRA(projectName: string, cwd: string): Promise<void> {
|
||||
console.log('Setting up CRA in ' + cwd);
|
||||
const setupProc = cp.spawn(
|
||||
'npx',
|
||||
['create-react-app', '--template', 'cra-template-typescript', projectName],
|
||||
{
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
setupProc.stdout.on('data', d => console.log(d.toString().replace(/\r?\n$/, '')));
|
||||
setupProc.stderr.on('data', d => console.error(d.toString().replace(/\r?\n$/, '')));
|
||||
|
||||
const done = getDeferred();
|
||||
setupProc.once('exit', () => {
|
||||
done.resolve(undefined);
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
|
||||
async function startDevServer(projectFolder: string): Promise<cp.ChildProcessWithoutNullStreams> {
|
||||
const devServerListening = getDeferred();
|
||||
const devServerProc = cp.spawn('npm', ['run-script', 'start'], {
|
||||
env: { ...process.env, BROWSER: 'none', SKIP_PREFLIGHT_CHECK: 'true' },
|
||||
cwd: projectFolder,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const timer = setTimeout(() => {
|
||||
console.log('Did not get recognized dev server output, continuing');
|
||||
devServerListening.resolve(undefined);
|
||||
}, 10000);
|
||||
devServerProc.stdout.on('data', d => {
|
||||
d = d.toString();
|
||||
if (d.includes('You can now view')) {
|
||||
console.log('Detected CRA dev server started');
|
||||
devServerListening.resolve(undefined);
|
||||
} else if (d.includes('Something is already')) {
|
||||
devServerListening.reject(new Error('Failed to start the dev server: ' + d));
|
||||
}
|
||||
|
||||
console.log(d.toString().replace(/\r?\n$/, ''));
|
||||
});
|
||||
devServerProc.stderr.on('data', d => console.error(d.toString().replace(/\r?\n$/, '')));
|
||||
await devServerListening.promise;
|
||||
clearTimeout(timer);
|
||||
|
||||
return devServerProc;
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче