OLD | NEW |
(Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 var TestRunner = class { |
| 6 constructor(baseURL, log, completeTest, fetch) { |
| 7 this._dumpInspectorProtocolMessages = false; |
| 8 this._baseURL = baseURL; |
| 9 this._log = log; |
| 10 this._completeTest = completeTest; |
| 11 this._fetch = fetch; |
| 12 } |
| 13 |
| 14 startDumpingProtocolMessages() { |
| 15 this._dumpInspectorProtocolMessages = true; |
| 16 }; |
| 17 |
| 18 completeTest() { |
| 19 this._completeTest.call(null); |
| 20 } |
| 21 |
| 22 log(text) { |
| 23 this._log.call(null, text); |
| 24 } |
| 25 |
| 26 logMessage(originalMessage) { |
| 27 var message = JSON.parse(JSON.stringify(originalMessage)); |
| 28 if (message.id) |
| 29 message.id = '<messageId>'; |
| 30 const nonStableFields = new Set(['nodeId', 'objectId', 'scriptId', 'timestam
p']); |
| 31 var objects = [message]; |
| 32 while (objects.length) { |
| 33 var object = objects.shift(); |
| 34 for (var key in object) { |
| 35 if (nonStableFields.has(key)) |
| 36 object[key] = `<${key}>`; |
| 37 else if (typeof object[key] === 'string' && object[key].match(/\d+:\d+:\
d+:debug/)) |
| 38 object[key] = object[key].replace(/\d+/, '<scriptId>'); |
| 39 else if (typeof object[key] === 'object') |
| 40 objects.push(object[key]); |
| 41 } |
| 42 } |
| 43 this.logObject(message); |
| 44 return originalMessage; |
| 45 } |
| 46 |
| 47 logObject(object, title) { |
| 48 var lines = []; |
| 49 |
| 50 function dumpValue(value, prefix, prefixWithName) { |
| 51 if (typeof value === 'object' && value !== null) { |
| 52 if (value instanceof Array) |
| 53 dumpItems(value, prefix, prefixWithName); |
| 54 else |
| 55 dumpProperties(value, prefix, prefixWithName); |
| 56 } else { |
| 57 lines.push(prefixWithName + String(value).replace(/\n/g, ' ')); |
| 58 } |
| 59 } |
| 60 |
| 61 function dumpProperties(object, prefix, firstLinePrefix) { |
| 62 prefix = prefix || ''; |
| 63 firstLinePrefix = firstLinePrefix || prefix; |
| 64 lines.push(firstLinePrefix + '{'); |
| 65 |
| 66 var propertyNames = Object.keys(object); |
| 67 propertyNames.sort(); |
| 68 for (var i = 0; i < propertyNames.length; ++i) { |
| 69 var name = propertyNames[i]; |
| 70 if (!object.hasOwnProperty(name)) |
| 71 continue; |
| 72 var prefixWithName = ' ' + prefix + name + ' : '; |
| 73 dumpValue(object[name], ' ' + prefix, prefixWithName); |
| 74 } |
| 75 lines.push(prefix + '}'); |
| 76 } |
| 77 |
| 78 function dumpItems(object, prefix, firstLinePrefix) { |
| 79 prefix = prefix || ''; |
| 80 firstLinePrefix = firstLinePrefix || prefix; |
| 81 lines.push(firstLinePrefix + '['); |
| 82 for (var i = 0; i < object.length; ++i) |
| 83 dumpValue(object[i], ' ' + prefix, ' ' + prefix + '[' + i + '] : '
); |
| 84 lines.push(prefix + ']'); |
| 85 } |
| 86 |
| 87 dumpValue(object, '', title || ''); |
| 88 this.log(lines.join('\n')); |
| 89 } |
| 90 |
| 91 url(relative) { |
| 92 return this._baseURL + relative; |
| 93 } |
| 94 |
| 95 _checkExpectation(fail, name, messageObject) { |
| 96 if (fail === !!messageObject.error) { |
| 97 this.log('PASS: ' + name); |
| 98 return true; |
| 99 } |
| 100 |
| 101 this.log('FAIL: ' + name + ': ' + JSON.stringify(messageObject)); |
| 102 this.completeTest(); |
| 103 return false; |
| 104 } |
| 105 |
| 106 expectedSuccess(name, messageObject) { |
| 107 return this._checkExpectation(false, name, messageObject); |
| 108 } |
| 109 |
| 110 expectedError(name, messageObject) { |
| 111 return this._checkExpectation(true, name, messageObject); |
| 112 } |
| 113 |
| 114 die(message, error) { |
| 115 this.log(`${message}: ${error}\n${error.stack}`); |
| 116 this.completeTest(); |
| 117 throw new Error(message); |
| 118 } |
| 119 |
| 120 async loadScript(url) { |
| 121 var source = await this._fetch(this.url(url)); |
| 122 return eval(`${source}\n//# sourceURL=${url}`); |
| 123 }; |
| 124 |
| 125 async createPage() { |
| 126 var targetId = (await DevToolsAPI._sendCommandOrDie('Target.createTarget', {
url: 'about:blank'})).targetId; |
| 127 await DevToolsAPI._sendCommandOrDie('Target.activateTarget', {targetId}); |
| 128 var page = new TestRunner.Page(this, targetId); |
| 129 var dummyURL = window.location.href; |
| 130 dummyURL = dummyURL.substring(0, dummyURL.indexOf('inspector-protocol-test.h
tml')) + 'inspector-protocol-page.html'; |
| 131 await page._navigate(dummyURL); |
| 132 return page; |
| 133 } |
| 134 |
| 135 async _start(description, html, url) { |
| 136 try { |
| 137 this.log(description); |
| 138 var page = await this.createPage(); |
| 139 if (url) |
| 140 await page.navigate(url); |
| 141 if (html) |
| 142 await page.loadHTML(html); |
| 143 var session = await page.createSession(); |
| 144 return { page: page, session: session, dp: session.protocol }; |
| 145 } catch (e) { |
| 146 this.die('Error starting the test', e); |
| 147 } |
| 148 }; |
| 149 |
| 150 startBlank(description) { |
| 151 return this._start(description, null, null); |
| 152 } |
| 153 |
| 154 startHTML(html, description) { |
| 155 return this._start(description, html, null); |
| 156 } |
| 157 |
| 158 startURL(url, description) { |
| 159 return this._start(description, null, url); |
| 160 } |
| 161 }; |
| 162 |
| 163 TestRunner.Page = class { |
| 164 constructor(testRunner, targetId) { |
| 165 this._testRunner = testRunner; |
| 166 this._targetId = targetId; |
| 167 } |
| 168 |
| 169 async createSession() { |
| 170 await DevToolsAPI._sendCommandOrDie('Target.attachToTarget', {targetId: this
._targetId}); |
| 171 var session = new TestRunner.Session(this); |
| 172 DevToolsAPI._sessions.set(this._targetId, session); |
| 173 return session; |
| 174 } |
| 175 |
| 176 navigate(url) { |
| 177 return this._navigate(this._testRunner.url(url)); |
| 178 } |
| 179 |
| 180 async _navigate(url) { |
| 181 if (DevToolsAPI._sessions.get(this._targetId)) |
| 182 this._testRunner.die(`Cannot navigate to ${url} with active session`, new
Error()); |
| 183 |
| 184 var session = await this.createSession(); |
| 185 session.protocol.Page.enable(); |
| 186 session.protocol.Page.navigate({url: url}); |
| 187 |
| 188 var callback; |
| 189 var promise = new Promise(f => callback = f); |
| 190 session.protocol.Page.onFrameNavigated(message => { |
| 191 if (!message.params.frame.parentId) |
| 192 callback(); |
| 193 }); |
| 194 await promise; |
| 195 |
| 196 await session.disconnect(); |
| 197 } |
| 198 |
| 199 async loadHTML(html) { |
| 200 if (DevToolsAPI._sessions.get(this._targetId)) |
| 201 this._testRunner.die('Cannot loadHTML with active session', new Error()); |
| 202 |
| 203 html = html.replace(/'/g, "\\'").replace(/\n/g, '\\n'); |
| 204 var session = await this.createSession(); |
| 205 await session.protocol.Runtime.evaluate({expression: `document.body.innerHTM
L='${html}'`}); |
| 206 await session.disconnect(); |
| 207 } |
| 208 }; |
| 209 |
| 210 TestRunner.Session = class { |
| 211 constructor(page) { |
| 212 this._testRunner = page._testRunner; |
| 213 this._page = page; |
| 214 this._requestId = 0; |
| 215 this._dispatchTable = new Map(); |
| 216 this._eventHandlers = new Map(); |
| 217 this.protocol = this._setupProtocol(); |
| 218 } |
| 219 |
| 220 async disconnect() { |
| 221 await DevToolsAPI._sendCommandOrDie('Target.detachFromTarget', {targetId: th
is._page._targetId}); |
| 222 DevToolsAPI._sessions.delete(this._page._targetId); |
| 223 } |
| 224 |
| 225 sendRawCommand(requestId, message) { |
| 226 DevToolsAPI._sendCommandOrDie('Target.sendMessageToTarget', {targetId: this.
_page._targetId, message: message}); |
| 227 return new Promise(f => this._dispatchTable.set(requestId, f)); |
| 228 } |
| 229 |
| 230 sendCommand(method, params) { |
| 231 var requestId = ++this._requestId; |
| 232 var messageObject = {'id': requestId, 'method': method, 'params': params}; |
| 233 if (this._testRunner._dumpInspectorProtocolMessages) |
| 234 this._testRunner.log(`frontend => backend: ${JSON.stringify(messageObject)
}`); |
| 235 return this.sendRawCommand(requestId, JSON.stringify(messageObject)); |
| 236 } |
| 237 |
| 238 async evaluate(code) { |
| 239 var response = await this.protocol.Runtime.evaluate({expression: code, retur
nByValue: true}); |
| 240 if (response.error) { |
| 241 this._testRunner.log(`Error while evaluating '${code}': ${response.error}`
); |
| 242 this._testRunner.completeTest(); |
| 243 } else { |
| 244 return response.result.result.value; |
| 245 } |
| 246 } |
| 247 |
| 248 async evaluateAsync(code) { |
| 249 var response = await this.protocol.Runtime.evaluate({expression: code, retur
nByValue: true, awaitPromise: true}); |
| 250 if (response.error) { |
| 251 this._testRunner.log(`Error while evaluating async '${code}': ${response.e
rror}`); |
| 252 this._testRunner.completeTest(); |
| 253 } else { |
| 254 return response.result.result.value; |
| 255 } |
| 256 } |
| 257 |
| 258 _dispatchMessage(message) { |
| 259 if (this._testRunner._dumpInspectorProtocolMessages) |
| 260 this._testRunner.log(`backend => frontend: ${JSON.stringify(message)}`); |
| 261 if (typeof message.id === 'number') { |
| 262 var handler = this._dispatchTable.get(message.id); |
| 263 if (handler) { |
| 264 this._dispatchTable.delete(message.id); |
| 265 handler(message); |
| 266 } |
| 267 } else { |
| 268 var eventName = message.method; |
| 269 for (var handler of (this._eventHandlers.get(eventName) || [])) |
| 270 handler(message); |
| 271 } |
| 272 } |
| 273 |
| 274 _setupProtocol() { |
| 275 return new Proxy({}, { get: (target, agentName, receiver) => new Proxy({}, { |
| 276 get: (target, methodName, receiver) => { |
| 277 const eventPattern = /^(on(ce)?|off)([A-Z][A-Za-z0-9]+)/; |
| 278 var match = eventPattern.exec(methodName); |
| 279 if (!match) |
| 280 return args => this.sendCommand(`${agentName}.${methodName}`, args ||
{}); |
| 281 var eventName = match[3]; |
| 282 eventName = eventName.charAt(0).toLowerCase() + eventName.slice(1); |
| 283 if (match[1] === 'once') |
| 284 return () => this._waitForEvent(`${agentName}.${eventName}`); |
| 285 if (match[1] === 'off') |
| 286 return listener => this._removeEventHandler(`${agentName}.${eventName}
`, listener); |
| 287 return listener => this._addEventHandler(`${agentName}.${eventName}`, li
stener); |
| 288 } |
| 289 })}); |
| 290 } |
| 291 |
| 292 _addEventHandler(eventName, handler) { |
| 293 var handlers = this._eventHandlers.get(eventName) || []; |
| 294 handlers.push(handler); |
| 295 this._eventHandlers.set(eventName, handlers); |
| 296 } |
| 297 |
| 298 _removeEventHandler(eventName, handler) { |
| 299 var handlers = this._eventHandlers.get(eventName) || []; |
| 300 var index = handlers.indexOf(handler); |
| 301 if (index === -1) |
| 302 return; |
| 303 handlers.splice(index, 1); |
| 304 this._eventHandlers.set(eventName, handlers); |
| 305 } |
| 306 |
| 307 _waitForEvent(eventName) { |
| 308 return new Promise(callback => { |
| 309 var handler = result => { |
| 310 this._removeEventHandler(eventName, handler); |
| 311 callback(result); |
| 312 }; |
| 313 this._addEventHandler(eventName, handler); |
| 314 }); |
| 315 } |
| 316 }; |
| 317 |
| 318 var DevToolsAPI = {}; |
| 319 DevToolsAPI._requestId = 0; |
| 320 DevToolsAPI._embedderMessageId = 0; |
| 321 DevToolsAPI._dispatchTable = new Map(); |
| 322 DevToolsAPI._sessions = new Map(); |
| 323 DevToolsAPI._outputElement = null; |
| 324 |
| 325 DevToolsAPI._log = function(text) { |
| 326 if (!DevToolsAPI._outputElement) { |
| 327 var intermediate = document.createElement('div'); |
| 328 document.body.appendChild(intermediate); |
| 329 var intermediate2 = document.createElement('div'); |
| 330 intermediate.appendChild(intermediate2); |
| 331 DevToolsAPI._outputElement = document.createElement('div'); |
| 332 DevToolsAPI._outputElement.className = 'output'; |
| 333 DevToolsAPI._outputElement.id = 'output'; |
| 334 DevToolsAPI._outputElement.style.whiteSpace = 'pre'; |
| 335 intermediate2.appendChild(DevToolsAPI._outputElement); |
| 336 } |
| 337 DevToolsAPI._outputElement.appendChild(document.createTextNode(text)); |
| 338 DevToolsAPI._outputElement.appendChild(document.createElement('br')); |
| 339 }; |
| 340 |
| 341 DevToolsAPI._completeTest = function() { |
| 342 window.testRunner.notifyDone(); |
| 343 }; |
| 344 |
| 345 DevToolsAPI._die = function(message, error) { |
| 346 DevToolsAPI._log(`${message}: ${error}\n${error.stack}`); |
| 347 DevToolsAPI._completeTest(); |
| 348 throw new Error(); |
| 349 }; |
| 350 |
| 351 DevToolsAPI.dispatchMessage = function(messageOrObject) { |
| 352 var messageObject = (typeof messageOrObject === 'string' ? JSON.parse(messageO
rObject) : messageOrObject); |
| 353 var messageId = messageObject.id; |
| 354 try { |
| 355 if (typeof messageId === 'number') { |
| 356 var handler = DevToolsAPI._dispatchTable.get(messageId); |
| 357 if (handler) { |
| 358 DevToolsAPI._dispatchTable.delete(messageId); |
| 359 handler(messageObject); |
| 360 } |
| 361 } else { |
| 362 var eventName = messageObject.method; |
| 363 if (eventName === 'Target.receivedMessageFromTarget') { |
| 364 var targetId = messageObject.params.targetId; |
| 365 var message = messageObject.params.message; |
| 366 var session = DevToolsAPI._sessions.get(targetId); |
| 367 if (session) |
| 368 session._dispatchMessage(JSON.parse(message)); |
| 369 } |
| 370 } |
| 371 } catch(e) { |
| 372 DevToolsAPI._die(`Exception when dispatching message\n${JSON.stringify(messa
geObject)}`, e); |
| 373 } |
| 374 }; |
| 375 |
| 376 DevToolsAPI._sendCommand = function(method, params) { |
| 377 var requestId = ++DevToolsAPI._requestId; |
| 378 var messageObject = {'id': requestId, 'method': method, 'params': params}; |
| 379 var embedderMessage = {'id': ++DevToolsAPI._embedderMessageId, 'method': 'disp
atchProtocolMessage', 'params': [JSON.stringify(messageObject)]}; |
| 380 DevToolsHost.sendMessageToEmbedder(JSON.stringify(embedderMessage)); |
| 381 return new Promise(f => DevToolsAPI._dispatchTable.set(requestId, f)); |
| 382 }; |
| 383 |
| 384 DevToolsAPI._sendCommandOrDie = function(method, params) { |
| 385 return DevToolsAPI._sendCommand(method, params).then(message => { |
| 386 if (message.error) |
| 387 DevToolsAPI._die('Error communicating with harness', new Error(message.err
or)); |
| 388 return message.result; |
| 389 }); |
| 390 }; |
| 391 |
| 392 function debugTest(testFunction) { |
| 393 var dispatch = DevToolsAPI.dispatchMessage; |
| 394 var messages = []; |
| 395 DevToolsAPI.dispatchMessage = message => { |
| 396 if (!messages.length) { |
| 397 setTimeout(() => { |
| 398 for (var message of messages.splice(0)) |
| 399 dispatch(message); |
| 400 }, 0); |
| 401 } |
| 402 messages.push(message); |
| 403 }; |
| 404 return testRunner => { |
| 405 testRunner.log = console.log; |
| 406 testRunner.completeTest = () => console.log('Test completed'); |
| 407 window.test = () => testFunction(testRunner); |
| 408 }; |
| 409 }; |
| 410 |
| 411 DevToolsAPI._fetch = function(url) { |
| 412 return new Promise(fulfill => { |
| 413 var xhr = new XMLHttpRequest(); |
| 414 xhr.open('GET', url, true); |
| 415 xhr.onreadystatechange = e => { |
| 416 if (xhr.readyState !== XMLHttpRequest.DONE) |
| 417 return; |
| 418 if ([0, 200, 304].indexOf(xhr.status) === -1) // Testing harness file:///
results in 0. |
| 419 DevToolsAPI._die(`${xhr.status} while fetching ${url}`, new Error()); |
| 420 else |
| 421 fulfill(e.target.response); |
| 422 }; |
| 423 xhr.send(null); |
| 424 }); |
| 425 }; |
| 426 |
| 427 window.testRunner.dumpAsText(); |
| 428 window.testRunner.waitUntilDone(); |
| 429 window.testRunner.setCanOpenWindows(true); |
| 430 |
| 431 window.addEventListener('load', () => { |
| 432 var testScriptURL = window.location.search.substring(1); |
| 433 var baseURL = testScriptURL.substring(0, testScriptURL.lastIndexOf('/') + 1); |
| 434 DevToolsAPI._fetch(testScriptURL).then(testScript => { |
| 435 var testRunner = new TestRunner(baseURL, DevToolsAPI._log, DevToolsAPI._comp
leteTest, DevToolsAPI._fetch); |
| 436 var testFunction = eval(`${testScript}\n//# sourceURL=${testScriptURL}`); |
| 437 return testFunction(testRunner); |
| 438 }).catch(reason => { |
| 439 DevToolsAPI._log(`Error while executing test script: ${reason}\n${reason.sta
ck}`); |
| 440 DevToolsAPI._completeTest(); |
| 441 }); |
| 442 }, false); |
| 443 |
| 444 window['onerror'] = (message, source, lineno, colno, error) => { |
| 445 DevToolsAPI._log(`${error}\n${error.stack}`); |
| 446 DevToolsAPI._completeTest(); |
| 447 }; |
| 448 |
| 449 window.addEventListener('unhandledrejection', e => { |
| 450 DevToolsAPI._log(`Promise rejection: ${e.reason}\n${e.reason ? e.reason.stack
: ''}`); |
| 451 DevToolsAPI._completeTest(); |
| 452 }, false); |
OLD | NEW |