1'use strict'; 2const common = require('../common'); 3 4common.skipIfInspectorDisabled(); 5 6const assert = require('assert'); 7const { NodeInstance } = require('../common/inspector-helper.js'); 8 9function checkListResponse(response) { 10 const expectedLength = 1; 11 assert.strictEqual( 12 response.length, 13 expectedLength, 14 `Expected response length ${response.length} to be ${expectedLength}.` 15 ); 16 assert.ok(response[0].devtoolsFrontendUrl); 17 assert.ok( 18 /ws:\/\/localhost:\d+\/[0-9A-Fa-f]{8}-/ 19 .test(response[0].webSocketDebuggerUrl), 20 response[0].webSocketDebuggerUrl); 21} 22 23function checkVersion(response) { 24 assert.ok(response); 25 const expected = { 26 'Browser': `node.js/${process.version}`, 27 'Protocol-Version': '1.1', 28 }; 29 assert.strictEqual(JSON.stringify(response), 30 JSON.stringify(expected)); 31} 32 33function checkBadPath(err) { 34 assert(err instanceof SyntaxError); 35 assert(/Unexpected token/.test(err.message), err.message); 36 assert(/WebSockets request was expected/.test(err.body), err.body); 37} 38 39function checkException(message) { 40 assert.strictEqual(message.exceptionDetails, undefined); 41} 42 43function assertScopeValues({ result }, expected) { 44 const unmatched = new Set(Object.keys(expected)); 45 for (const actual of result) { 46 const value = expected[actual.name]; 47 if (value) { 48 assert.strictEqual( 49 actual.value.value, 50 value, 51 `Expected scope values to be ${actual.value.value} instead of ${value}.` 52 ); 53 unmatched.delete(actual.name); 54 } 55 } 56 if (unmatched.size) 57 assert.fail(Array.from(unmatched.values())); 58} 59 60async function testBreakpointOnStart(session) { 61 console.log('[test]', 62 'Verifying debugger stops on start (--inspect-brk option)'); 63 const commands = [ 64 { 'method': 'Runtime.enable' }, 65 { 'method': 'Debugger.enable' }, 66 { 'method': 'Debugger.setPauseOnExceptions', 67 'params': { 'state': 'none' } }, 68 { 'method': 'Debugger.setAsyncCallStackDepth', 69 'params': { 'maxDepth': 0 } }, 70 { 'method': 'Profiler.enable' }, 71 { 'method': 'Profiler.setSamplingInterval', 72 'params': { 'interval': 100 } }, 73 { 'method': 'Debugger.setBlackboxPatterns', 74 'params': { 'patterns': [] } }, 75 { 'method': 'Runtime.runIfWaitingForDebugger' } 76 ]; 77 78 await session.send(commands); 79 await session.waitForBreakOnLine(0, session.scriptURL()); 80} 81 82async function testBreakpoint(session) { 83 console.log('[test]', 'Setting a breakpoint and verifying it is hit'); 84 const commands = [ 85 { 'method': 'Debugger.setBreakpointByUrl', 86 'params': { 'lineNumber': 5, 87 'url': session.scriptURL(), 88 'columnNumber': 0, 89 'condition': '' 90 } 91 }, 92 { 'method': 'Debugger.resume' }, 93 ]; 94 await session.send(commands); 95 const { scriptSource } = await session.send({ 96 'method': 'Debugger.getScriptSource', 97 'params': { 'scriptId': session.mainScriptId } }); 98 assert(scriptSource && (scriptSource.includes(session.script())), 99 `Script source is wrong: ${scriptSource}`); 100 101 await session.waitForConsoleOutput('log', ['A message', 5]); 102 const paused = await session.waitForBreakOnLine(5, session.scriptURL()); 103 const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; 104 105 console.log('[test]', 'Verify we can read current application state'); 106 const response = await session.send({ 107 'method': 'Runtime.getProperties', 108 'params': { 109 'objectId': scopeId, 110 'ownProperties': false, 111 'accessorPropertiesOnly': false, 112 'generatePreview': true 113 } 114 }); 115 assertScopeValues(response, { t: 1001, k: 1 }); 116 117 let { result } = await session.send({ 118 'method': 'Debugger.evaluateOnCallFrame', 'params': { 119 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 120 'expression': 'k + t', 121 'objectGroup': 'console', 122 'includeCommandLineAPI': true, 123 'silent': false, 124 'returnByValue': false, 125 'generatePreview': true 126 } 127 }); 128 const expectedEvaluation = 1002; 129 assert.strictEqual( 130 result.value, 131 expectedEvaluation, 132 `Expected evaluation to be ${expectedEvaluation}, got ${result.value}.` 133 ); 134 135 result = (await session.send({ 136 'method': 'Runtime.evaluate', 'params': { 137 'expression': '5 * 5' 138 } 139 })).result; 140 const expectedResult = 25; 141 assert.strictEqual( 142 result.value, 143 expectedResult, 144 `Expected Runtime.evaluate to be ${expectedResult}, got ${result.value}.` 145 ); 146} 147 148async function testI18NCharacters(session) { 149 console.log('[test]', 'Verify sending and receiving UTF8 characters'); 150 const chars = 'טֶ字и'; 151 session.send({ 152 'method': 'Debugger.evaluateOnCallFrame', 'params': { 153 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 154 'expression': `console.log("${chars}")`, 155 'objectGroup': 'console', 156 'includeCommandLineAPI': true, 157 'silent': false, 158 'returnByValue': false, 159 'generatePreview': true 160 } 161 }); 162 await session.waitForConsoleOutput('log', [chars]); 163} 164 165async function testCommandLineAPI(session) { 166 const testModulePath = require.resolve('../fixtures/empty.js'); 167 const testModuleStr = JSON.stringify(testModulePath); 168 const printAModulePath = require.resolve('../fixtures/printA.js'); 169 const printAModuleStr = JSON.stringify(printAModulePath); 170 const printBModulePath = require.resolve('../fixtures/printB.js'); 171 const printBModuleStr = JSON.stringify(printBModulePath); 172 173 // We can use `require` outside of a callframe with require in scope 174 let result = await session.send( 175 { 176 'method': 'Runtime.evaluate', 'params': { 177 'expression': 'typeof require("fs").readFile === "function"', 178 'includeCommandLineAPI': true 179 } 180 }); 181 checkException(result); 182 assert.strictEqual(result.result.value, true); 183 184 // The global require has the same properties as a normal `require` 185 result = await session.send( 186 { 187 'method': 'Runtime.evaluate', 'params': { 188 'expression': [ 189 'typeof require.resolve === "function"', 190 'typeof require.extensions === "object"', 191 'typeof require.cache === "object"' 192 ].join(' && '), 193 'includeCommandLineAPI': true 194 } 195 }); 196 checkException(result); 197 assert.strictEqual(result.result.value, true); 198 // `require` twice returns the same value 199 result = await session.send( 200 { 201 'method': 'Runtime.evaluate', 'params': { 202 // 1. We require the same module twice 203 // 2. We mutate the exports so we can compare it later on 204 'expression': ` 205 Object.assign( 206 require(${testModuleStr}), 207 { old: 'yes' } 208 ) === require(${testModuleStr})`, 209 'includeCommandLineAPI': true 210 } 211 }); 212 checkException(result); 213 assert.strictEqual(result.result.value, true); 214 // After require the module appears in require.cache 215 result = await session.send( 216 { 217 'method': 'Runtime.evaluate', 'params': { 218 'expression': `JSON.stringify( 219 require.cache[${testModuleStr}].exports 220 )`, 221 'includeCommandLineAPI': true 222 } 223 }); 224 checkException(result); 225 assert.deepStrictEqual(JSON.parse(result.result.value), 226 { old: 'yes' }); 227 // Remove module from require.cache 228 result = await session.send( 229 { 230 'method': 'Runtime.evaluate', 'params': { 231 'expression': `delete require.cache[${testModuleStr}]`, 232 'includeCommandLineAPI': true 233 } 234 }); 235 checkException(result); 236 assert.strictEqual(result.result.value, true); 237 // Require again, should get fresh (empty) exports 238 result = await session.send( 239 { 240 'method': 'Runtime.evaluate', 'params': { 241 'expression': `JSON.stringify(require(${testModuleStr}))`, 242 'includeCommandLineAPI': true 243 } 244 }); 245 checkException(result); 246 assert.deepStrictEqual(JSON.parse(result.result.value), {}); 247 // require 2nd module, exports an empty object 248 result = await session.send( 249 { 250 'method': 'Runtime.evaluate', 'params': { 251 'expression': `JSON.stringify(require(${printAModuleStr}))`, 252 'includeCommandLineAPI': true 253 } 254 }); 255 checkException(result); 256 assert.deepStrictEqual(JSON.parse(result.result.value), {}); 257 // Both modules end up with the same module.parent 258 result = await session.send( 259 { 260 'method': 'Runtime.evaluate', 'params': { 261 'expression': `JSON.stringify({ 262 parentsEqual: 263 require.cache[${testModuleStr}].parent === 264 require.cache[${printAModuleStr}].parent, 265 parentId: require.cache[${testModuleStr}].parent.id, 266 })`, 267 'includeCommandLineAPI': true 268 } 269 }); 270 checkException(result); 271 assert.deepStrictEqual(JSON.parse(result.result.value), { 272 parentsEqual: true, 273 parentId: '<inspector console>' 274 }); 275 // The `require` in the module shadows the command line API's `require` 276 result = await session.send( 277 { 278 'method': 'Debugger.evaluateOnCallFrame', 'params': { 279 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 280 'expression': `( 281 require(${printBModuleStr}), 282 require.cache[${printBModuleStr}].parent.id 283 )`, 284 'includeCommandLineAPI': true 285 } 286 }); 287 checkException(result); 288 assert.notStrictEqual(result.result.value, 289 '<inspector console>'); 290} 291 292async function runTest() { 293 const child = new NodeInstance(); 294 checkListResponse(await child.httpGet(null, '/json')); 295 checkListResponse(await child.httpGet(null, '/json/list')); 296 checkVersion(await child.httpGet(null, '/json/version')); 297 298 await child.httpGet(null, '/json/activate').catch(checkBadPath); 299 await child.httpGet(null, '/json/activate/boom').catch(checkBadPath); 300 await child.httpGet(null, '/json/badpath').catch(checkBadPath); 301 302 const session = await child.connectInspectorSession(); 303 await testBreakpointOnStart(session); 304 await testBreakpoint(session); 305 await testI18NCharacters(session); 306 await testCommandLineAPI(session); 307 await session.runToCompletion(); 308 const expectedExitCode = 55; 309 const { exitCode } = await child.expectShutdown(); 310 assert.strictEqual( 311 exitCode, 312 expectedExitCode, 313 `Expected exit code to be ${expectedExitCode} but got ${expectedExitCode}.` 314 ); 315} 316 317runTest().then(common.mustCall()); 318