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 { 'method': 'Debugger.resume' }, 91 ]; 92 await session.send(commands); 93 const { scriptSource } = await session.send({ 94 'method': 'Debugger.getScriptSource', 95 'params': { 'scriptId': session.mainScriptId }, 96 }); 97 assert(scriptSource && (scriptSource.includes(session.script())), 98 `Script source is wrong: ${scriptSource}`); 99 100 await session.waitForConsoleOutput('log', ['A message', 5]); 101 const paused = await session.waitForBreakOnLine(5, session.scriptURL()); 102 const scopeId = paused.params.callFrames[0].scopeChain[0].object.objectId; 103 104 console.log('[test]', 'Verify we can read current application state'); 105 const response = await session.send({ 106 'method': 'Runtime.getProperties', 107 'params': { 108 'objectId': scopeId, 109 'ownProperties': false, 110 'accessorPropertiesOnly': false, 111 'generatePreview': true 112 } 113 }); 114 assertScopeValues(response, { t: 1001, k: 1 }); 115 116 let { result } = await session.send({ 117 'method': 'Debugger.evaluateOnCallFrame', 'params': { 118 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 119 'expression': 'k + t', 120 'objectGroup': 'console', 121 'includeCommandLineAPI': true, 122 'silent': false, 123 'returnByValue': false, 124 'generatePreview': true 125 } 126 }); 127 const expectedEvaluation = 1002; 128 assert.strictEqual( 129 result.value, 130 expectedEvaluation, 131 `Expected evaluation to be ${expectedEvaluation}, got ${result.value}.` 132 ); 133 134 result = (await session.send({ 135 'method': 'Runtime.evaluate', 'params': { 136 'expression': '5 * 5' 137 } 138 })).result; 139 const expectedResult = 25; 140 assert.strictEqual( 141 result.value, 142 expectedResult, 143 `Expected Runtime.evaluate to be ${expectedResult}, got ${result.value}.` 144 ); 145} 146 147async function testI18NCharacters(session) { 148 console.log('[test]', 'Verify sending and receiving UTF8 characters'); 149 const chars = 'טֶ字и'; 150 session.send({ 151 'method': 'Debugger.evaluateOnCallFrame', 'params': { 152 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 153 'expression': `console.log("${chars}")`, 154 'objectGroup': 'console', 155 'includeCommandLineAPI': true, 156 'silent': false, 157 'returnByValue': false, 158 'generatePreview': true 159 } 160 }); 161 await session.waitForConsoleOutput('log', [chars]); 162} 163 164async function testCommandLineAPI(session) { 165 const testModulePath = require.resolve('../fixtures/empty.js'); 166 const testModuleStr = JSON.stringify(testModulePath); 167 const printAModulePath = require.resolve('../fixtures/printA.js'); 168 const printAModuleStr = JSON.stringify(printAModulePath); 169 const printBModulePath = require.resolve('../fixtures/printB.js'); 170 const printBModuleStr = JSON.stringify(printBModulePath); 171 172 // We can use `require` outside of a callframe with require in scope 173 let result = await session.send( 174 { 175 'method': 'Runtime.evaluate', 'params': { 176 'expression': 'typeof require("fs").readFile === "function"', 177 'includeCommandLineAPI': true 178 } 179 }); 180 checkException(result); 181 assert.strictEqual(result.result.value, true); 182 183 // The global require has the same properties as a normal `require` 184 result = await session.send( 185 { 186 'method': 'Runtime.evaluate', 'params': { 187 'expression': [ 188 'typeof require.resolve === "function"', 189 'typeof require.extensions === "object"', 190 'typeof require.cache === "object"', 191 ].join(' && '), 192 'includeCommandLineAPI': true 193 } 194 }); 195 checkException(result); 196 assert.strictEqual(result.result.value, true); 197 // `require` twice returns the same value 198 result = await session.send( 199 { 200 'method': 'Runtime.evaluate', 'params': { 201 // 1. We require the same module twice 202 // 2. We mutate the exports so we can compare it later on 203 'expression': ` 204 Object.assign( 205 require(${testModuleStr}), 206 { old: 'yes' } 207 ) === require(${testModuleStr})`, 208 'includeCommandLineAPI': true 209 } 210 }); 211 checkException(result); 212 assert.strictEqual(result.result.value, true); 213 // After require the module appears in require.cache 214 result = await session.send( 215 { 216 'method': 'Runtime.evaluate', 'params': { 217 'expression': `JSON.stringify( 218 require.cache[${testModuleStr}].exports 219 )`, 220 'includeCommandLineAPI': true 221 } 222 }); 223 checkException(result); 224 assert.deepStrictEqual(JSON.parse(result.result.value), 225 { old: 'yes' }); 226 // Remove module from require.cache 227 result = await session.send( 228 { 229 'method': 'Runtime.evaluate', 'params': { 230 'expression': `delete require.cache[${testModuleStr}]`, 231 'includeCommandLineAPI': true 232 } 233 }); 234 checkException(result); 235 assert.strictEqual(result.result.value, true); 236 // Require again, should get fresh (empty) exports 237 result = await session.send( 238 { 239 'method': 'Runtime.evaluate', 'params': { 240 'expression': `JSON.stringify(require(${testModuleStr}))`, 241 'includeCommandLineAPI': true 242 } 243 }); 244 checkException(result); 245 assert.deepStrictEqual(JSON.parse(result.result.value), {}); 246 // require 2nd module, exports an empty object 247 result = await session.send( 248 { 249 'method': 'Runtime.evaluate', 'params': { 250 'expression': `JSON.stringify(require(${printAModuleStr}))`, 251 'includeCommandLineAPI': true 252 } 253 }); 254 checkException(result); 255 assert.deepStrictEqual(JSON.parse(result.result.value), {}); 256 // Both modules end up with the same module.parent 257 result = await session.send( 258 { 259 'method': 'Runtime.evaluate', 'params': { 260 'expression': `JSON.stringify({ 261 parentsEqual: 262 require.cache[${testModuleStr}].parent === 263 require.cache[${printAModuleStr}].parent, 264 parentId: require.cache[${testModuleStr}].parent.id, 265 })`, 266 'includeCommandLineAPI': true 267 } 268 }); 269 checkException(result); 270 assert.deepStrictEqual(JSON.parse(result.result.value), { 271 parentsEqual: true, 272 parentId: '<inspector console>' 273 }); 274 // The `require` in the module shadows the command line API's `require` 275 result = await session.send( 276 { 277 'method': 'Debugger.evaluateOnCallFrame', 'params': { 278 'callFrameId': session.pausedDetails().callFrames[0].callFrameId, 279 'expression': `( 280 require(${printBModuleStr}), 281 require.cache[${printBModuleStr}].parent.id 282 )`, 283 'includeCommandLineAPI': true 284 } 285 }); 286 checkException(result); 287 assert.notStrictEqual(result.result.value, 288 '<inspector console>'); 289} 290 291async function runTest() { 292 const child = new NodeInstance(); 293 checkListResponse(await child.httpGet(null, '/json')); 294 checkListResponse(await child.httpGet(null, '/json/list')); 295 checkVersion(await child.httpGet(null, '/json/version')); 296 297 await child.httpGet(null, '/json/activate').catch(checkBadPath); 298 await child.httpGet(null, '/json/activate/boom').catch(checkBadPath); 299 await child.httpGet(null, '/json/badpath').catch(checkBadPath); 300 301 const session = await child.connectInspectorSession(); 302 await testBreakpointOnStart(session); 303 await testBreakpoint(session); 304 await testI18NCharacters(session); 305 await testCommandLineAPI(session); 306 await session.runToCompletion(); 307 const expectedExitCode = 55; 308 const { exitCode } = await child.expectShutdown(); 309 assert.strictEqual( 310 exitCode, 311 expectedExitCode, 312 `Expected exit code to be ${expectedExitCode} but got ${expectedExitCode}.` 313 ); 314} 315 316runTest().then(common.mustCall()); 317