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(); | |
einbinder
2017/06/21 01:21:48
Put the message in this error?
dgozman
2017/06/21 18:11:47
Done.
| |
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, errorMessage) { | |
385 errorMessage = errorMessage || 'Unexpected error'; | |
einbinder
2017/06/21 01:21:48
errorMessage doesn't appear to be used anywhere.
dgozman
2017/06/21 18:11:47
Removed.
| |
386 return DevToolsAPI._sendCommand(method, params).then(message => { | |
387 if (message.error) | |
388 DevToolsAPI._die('Error communicating with harness', new Error(message.err or)); | |
389 return message.result; | |
390 }); | |
391 }; | |
392 | |
393 function debugTest(testFunction) { | |
394 var dispatch = DevToolsAPI.dispatchMessage; | |
395 var messages = []; | |
396 DevToolsAPI.dispatchMessage = message => { | |
397 if (!messages.length) { | |
398 setTimeout(() => { | |
399 for (var message of messages.splice(0)) | |
400 dispatch(message); | |
401 }, 0); | |
402 } | |
403 messages.push(message); | |
404 }; | |
405 return testRunner => { | |
406 testRunner.log = console.log; | |
407 testRunner.completeTest = () => console.log('Test completed'); | |
408 window.test = () => testFunction(testRunner); | |
409 }; | |
410 }; | |
411 | |
412 DevToolsAPI._fetch = function(url) { | |
413 return new Promise(fulfill => { | |
414 var xhr = new XMLHttpRequest(); | |
415 xhr.open('GET', url, true); | |
416 xhr.onreadystatechange = e => { | |
417 if (xhr.readyState !== XMLHttpRequest.DONE) | |
418 return; | |
419 if ([0, 200, 304].indexOf(xhr.status) === -1) // Testing harness file:/// results in 0. | |
420 DevToolsAPI._die(`${xhr.status} while fetching ${url}`, new Error()); | |
421 else | |
422 fulfill(e.target.response); | |
423 }; | |
424 xhr.send(null); | |
425 }); | |
426 }; | |
427 | |
428 if (window.testRunner) { | |
einbinder
2017/06/21 01:21:48
Throw if no testRunner?
dgozman
2017/06/21 18:11:47
Done.
| |
429 window.testRunner.dumpAsText(); | |
430 window.testRunner.waitUntilDone(); | |
431 window.testRunner.setCanOpenWindows(true); | |
432 } | |
433 | |
434 window.addEventListener('load', () => { | |
435 var testScriptURL = window.location.search.substring(1); | |
436 var baseURL = testScriptURL.substring(0, testScriptURL.lastIndexOf('/') + 1); | |
437 DevToolsAPI._fetch(testScriptURL).then(testScript => { | |
438 var testRunner = new TestRunner(baseURL, DevToolsAPI._log, DevToolsAPI._comp leteTest, DevToolsAPI._fetch); | |
439 var testFunction = eval(`${testScript}\n//# sourceURL=${testScriptURL}`); | |
440 return testFunction(testRunner); | |
441 }).catch(reason => { | |
442 DevToolsAPI._log(`Error while executing test script: ${reason}\n${reason.sta ck}`); | |
443 DevToolsAPI._completeTest(); | |
444 }); | |
445 }, false); | |
446 | |
447 window['onerror'] = (message, source, lineno, colno, error) => { | |
448 DevToolsAPI._log(`${error}\n${error.stack}`); | |
einbinder
2017/06/21 01:21:48
Check that error isn't null/undefined here. Otherw
dgozman
2017/06/21 18:11:47
I think this one is an Error instance. I do check
| |
449 DevToolsAPI._completeTest(); | |
450 }; | |
451 | |
452 window.addEventListener('unhandledrejection', e => { | |
453 DevToolsAPI._log(`Promise rejection: ${e.reason}\n${e.reason ? e.reason.stack : ''}`); | |
454 DevToolsAPI._completeTest(); | |
455 }, false); | |
OLD | NEW |