'use strict'; const common = require('../common'); common.skipIfInspectorDisabled(); const assert = require('assert'); const { NodeInstance } = require('../common/inspector-helper.js'); function checkListResponse(response) { const expectedLength = 1; assert.strictEqual( response.length, expectedLength, `Expected response length ${response.length} to be ${expectedLength}.` ); assert.ok(response[0].devtoolsFrontendUrl); assert.ok( /ws:\/\/localhost:\d+\/[0-9A-Fa-f]{8}-/ .test(response[0].webSocketDebuggerUrl), response[0].webSocketDebuggerUrl); } function checkVersion(response) { assert.ok(response); const expected = { 'Browser': `node.js/${process.version}`, 'Protocol-Version': '1.1', }; assert.strictEqual(JSON.stringify(response), JSON.stringify(expected)); } function checkBadPath(err) { assert(err instanceof SyntaxError); assert.match(err.message, /Unexpected token/); assert.match(err.body, /WebSockets request was expected/); } function checkException(message) { assert.strictEqual(message.exceptionDetails, undefined); } function assertScopeValues({ result }, expected) { const unmatched = new Set(Object.keys(expected)); for (const actual of result) { const value = expected[actual.name]; if (value) { assert.strictEqual( actual.value.value, value, `Expected scope values to be ${actual.value.value} instead of ${value}.` ); unmatched.delete(actual.name); } } if (unmatched.size) assert.fail(Array.from(unmatched.values())); } async function testBreakpointOnStart(session) { console.log('[test]', 'Verifying debugger stops on start (--inspect-brk option)'); const commands = [ { 'method': 'Runtime.enable' }, { 'method': 'Debugger.enable' }, { 'method': 'Debugger.setPauseOnExceptions', 'params': { 'state': 'none' } }, { 'method': 'Debugger.setAsyncCallStackDepth', 'params': { 'maxDepth': 0 } }, { 'method': 'Profiler.enable' }, { 'method': 'Profiler.setSamplingInterval', 'params': { 'interval': 100 } }, { 'method': 'Debugger.setBlackboxPatterns', 'params': { 'patterns': [] } }, { 'method': 'Runtime.runIfWaitingForDebugger' }, ]; await session.send({ method: 'NodeRuntime.enable' }); await session.waitForNotification('NodeRuntime.waitingForDebugger'); await session.send(commands); await session.send({ method: 'NodeRuntime.disable' }); await session.waitForBreakOnLine(0, session.scriptURL()); } async function testBreakpoint(session) { console.log('[test]', 'Setting a breakpoint and verifying it is hit'); const commands = [ { 'method': 'Debugger.setBreakpointByUrl', 'params': { 'lineNumber': 5, 'url': session.scriptURL(), 'columnNumber': 0, 'condition': '' } }, { 'method': 'Debugger.resume' }, ]; await session.send(commands); const { scriptSource } = await session.send({ 'method': 'Debugger.getScriptSource', 'params': { 'scriptId': session.mainScriptId }, }); assert(scriptSource && (scriptSource.includes(session.script())), `Script source is wrong: ${scriptSource}`); await session.waitForConsoleOutput('log', ['A message', 5]); const paused = await session.waitForBreakOnLine(5, session.scriptURL()); const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; console.log('[test]', 'Verify we can read current application state'); const response = await session.send({ 'method': 'Runtime.getProperties', 'params': { 'objectId': scopeId, 'ownProperties': false, 'accessorPropertiesOnly': false, 'generatePreview': true } }); assertScopeValues(response, { t: 1001, k: 1 }); let { result } = await session.send({ 'method': 'Debugger.evaluateOnCallFrame', 'params': { 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 'expression': 'k + t', 'objectGroup': 'console', 'includeCommandLineAPI': true, 'silent': false, 'returnByValue': false, 'generatePreview': true } }); const expectedEvaluation = 1002; assert.strictEqual( result.value, expectedEvaluation, `Expected evaluation to be ${expectedEvaluation}, got ${result.value}.` ); result = (await session.send({ 'method': 'Runtime.evaluate', 'params': { 'expression': '5 * 5' } })).result; const expectedResult = 25; assert.strictEqual( result.value, expectedResult, `Expected Runtime.evaluate to be ${expectedResult}, got ${result.value}.` ); } async function testI18NCharacters(session) { console.log('[test]', 'Verify sending and receiving UTF8 characters'); const chars = 'טֶ字и'; session.send({ 'method': 'Debugger.evaluateOnCallFrame', 'params': { 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 'expression': `console.log("${chars}")`, 'objectGroup': 'console', 'includeCommandLineAPI': true, 'silent': false, 'returnByValue': false, 'generatePreview': true } }); await session.waitForConsoleOutput('log', [chars]); } async function testCommandLineAPI(session) { const testModulePath = require.resolve('../fixtures/empty.js'); const testModuleStr = JSON.stringify(testModulePath); const printAModulePath = require.resolve('../fixtures/printA.js'); const printAModuleStr = JSON.stringify(printAModulePath); const printBModulePath = require.resolve('../fixtures/printB.js'); const printBModuleStr = JSON.stringify(printBModulePath); // We can use `require` outside of a callframe with require in scope let result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': 'typeof require("fs").readFile === "function"', 'includeCommandLineAPI': true } }); checkException(result); assert.strictEqual(result.result.value, true); // The global require has the same properties as a normal `require` result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': [ 'typeof require.resolve === "function"', 'typeof require.extensions === "object"', 'typeof require.cache === "object"', ].join(' && '), 'includeCommandLineAPI': true } }); checkException(result); assert.strictEqual(result.result.value, true); // `require` twice returns the same value result = await session.send( { 'method': 'Runtime.evaluate', 'params': { // 1. We require the same module twice // 2. We mutate the exports so we can compare it later on 'expression': ` Object.assign( require(${testModuleStr}), { old: 'yes' } ) === require(${testModuleStr})`, 'includeCommandLineAPI': true } }); checkException(result); assert.strictEqual(result.result.value, true); // After require the module appears in require.cache result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': `JSON.stringify( require.cache[${testModuleStr}].exports )`, 'includeCommandLineAPI': true } }); checkException(result); assert.deepStrictEqual(JSON.parse(result.result.value), { old: 'yes' }); // Remove module from require.cache result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': `delete require.cache[${testModuleStr}]`, 'includeCommandLineAPI': true } }); checkException(result); assert.strictEqual(result.result.value, true); // Require again, should get fresh (empty) exports result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': `JSON.stringify(require(${testModuleStr}))`, 'includeCommandLineAPI': true } }); checkException(result); assert.deepStrictEqual(JSON.parse(result.result.value), {}); // require 2nd module, exports an empty object result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': `JSON.stringify(require(${printAModuleStr}))`, 'includeCommandLineAPI': true } }); checkException(result); assert.deepStrictEqual(JSON.parse(result.result.value), {}); // Both modules end up with the same module.parent result = await session.send( { 'method': 'Runtime.evaluate', 'params': { 'expression': `JSON.stringify({ parentsEqual: require.cache[${testModuleStr}].parent === require.cache[${printAModuleStr}].parent, parentId: require.cache[${testModuleStr}].parent.id, })`, 'includeCommandLineAPI': true } }); checkException(result); assert.deepStrictEqual(JSON.parse(result.result.value), { parentsEqual: true, parentId: '' }); // The `require` in the module shadows the command line API's `require` result = await session.send( { 'method': 'Debugger.evaluateOnCallFrame', 'params': { 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 'expression': `( require(${printBModuleStr}), require.cache[${printBModuleStr}].parent.id )`, 'includeCommandLineAPI': true } }); checkException(result); assert.notStrictEqual(result.result.value, ''); } async function runTest() { const child = new NodeInstance(); checkListResponse(await child.httpGet(null, '/json')); checkListResponse(await child.httpGet(null, '/json/list')); checkVersion(await child.httpGet(null, '/json/version')); await child.httpGet(null, '/json/activate').catch(checkBadPath); await child.httpGet(null, '/json/activate/boom').catch(checkBadPath); await child.httpGet(null, '/json/badpath').catch(checkBadPath); const session = await child.connectInspectorSession(); await testBreakpointOnStart(session); await testBreakpoint(session); await testI18NCharacters(session); await testCommandLineAPI(session); await session.runToCompletion(); const expectedExitCode = 55; const { exitCode } = await child.expectShutdown(); assert.strictEqual( exitCode, expectedExitCode, `Expected exit code to be ${expectedExitCode} but got ${expectedExitCode}.` ); } runTest().then(common.mustCall());