OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (C) 2011 Google Inc. All rights reserved. | |
3 * | |
4 * Redistribution and use in source and binary forms, with or without | |
5 * modification, are permitted provided that the following conditions | |
6 * are met: | |
7 * 1. Redistributions of source code must retain the above copyright | |
8 * notice, this list of conditions and the following disclaimer. | |
9 * 2. Redistributions in binary form must reproduce the above copyright | |
10 * notice, this list of conditions and the following disclaimer in the | |
11 * documentation and/or other materials provided with the distribution. | |
12 * | |
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | |
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | |
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | |
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | |
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
23 * THE POSSIBILITY OF SUCH DAMAGE. | |
24 */ | |
25 | |
26 var ui = ui || {}; | |
27 ui.results = ui.results || {}; | |
28 | |
29 (function(){ | |
30 | |
31 var kResultsPrefetchDelayMS = 500; | |
32 | |
33 // FIXME: Rather than using table, should we be using something fancier? | |
34 ui.results.Comparison = base.extends('table', { | |
35 init: function() | |
36 { | |
37 this.className = 'comparison'; | |
38 this.innerHTML = '<thead><tr><th>Expected</th><th>Actual</th><th>Diff</t
h></tr></thead>' + | |
39 '<tbody><tr><td class="expected result-container"></td>
<td class="actual result-container"></td><td class="diff result-container"></td>
</tr></tbody>'; | |
40 }, | |
41 _selectorForKind: function(kind) | |
42 { | |
43 switch (kind) { | |
44 case results.kExpectedKind: | |
45 return '.expected'; | |
46 case results.kActualKind: | |
47 return '.actual'; | |
48 case results.kDiffKind: | |
49 return '.diff'; | |
50 } | |
51 return '.unknown'; | |
52 }, | |
53 update: function(kind, result) | |
54 { | |
55 var selector = this._selectorForKind(kind); | |
56 $(selector, this).empty().append(result); | |
57 return result; | |
58 }, | |
59 }); | |
60 | |
61 // We'd really like TextResult and ImageResult to extend a common Result base | |
62 // class, but we can't seem to do that because they inherit from different | |
63 // HTMLElements. We could have them inherit from <div>, but that seems lame. | |
64 | |
65 ui.results.TextResult = base.extends('iframe', { | |
66 init: function(url) | |
67 { | |
68 this.className = 'text-result'; | |
69 this.src = url; | |
70 } | |
71 }); | |
72 | |
73 ui.results.ImageResult = base.extends('img', { | |
74 init: function(url) | |
75 { | |
76 this.className = 'image-result'; | |
77 this.src = url; | |
78 } | |
79 }); | |
80 | |
81 ui.results.AudioResult = base.extends('audio', { | |
82 init: function(url) | |
83 { | |
84 this.className = 'audio-result'; | |
85 this.src = url; | |
86 this.controls = 'controls'; | |
87 } | |
88 }); | |
89 | |
90 function constructorForResultType(type) | |
91 { | |
92 if (type == results.kImageType) | |
93 return ui.results.ImageResult; | |
94 if (type == results.kAudioType) | |
95 return ui.results.AudioResult; | |
96 return ui.results.TextResult; | |
97 } | |
98 | |
99 ui.results.ResultsGrid = base.extends('div', { | |
100 init: function() | |
101 { | |
102 this.className = 'results-grid'; | |
103 }, | |
104 _addResult: function(comparison, constructor, resultsURLsByKind, kind) | |
105 { | |
106 var url = resultsURLsByKind[kind]; | |
107 if (!url) | |
108 return; | |
109 comparison.update(kind, new constructor(url)); | |
110 }, | |
111 addComparison: function(resultType, resultsURLsByKind) | |
112 { | |
113 var comparison = new ui.results.Comparison(); | |
114 var constructor = constructorForResultType(resultType); | |
115 | |
116 this._addResult(comparison, constructor, resultsURLsByKind, results.kExp
ectedKind); | |
117 this._addResult(comparison, constructor, resultsURLsByKind, results.kAct
ualKind); | |
118 this._addResult(comparison, constructor, resultsURLsByKind, results.kDif
fKind); | |
119 | |
120 this.appendChild(comparison); | |
121 return comparison; | |
122 }, | |
123 addRow: function(resultType, url) | |
124 { | |
125 var constructor = constructorForResultType(resultType); | |
126 var view = new constructor(url); | |
127 this.appendChild(view); | |
128 return view; | |
129 }, | |
130 addResults: function(resultsURLs) | |
131 { | |
132 var resultsURLsByTypeAndKind = {}; | |
133 | |
134 resultsURLsByTypeAndKind[results.kImageType] = {}; | |
135 resultsURLsByTypeAndKind[results.kAudioType] = {}; | |
136 resultsURLsByTypeAndKind[results.kTextType] = {}; | |
137 | |
138 resultsURLs.forEach(function(url) { | |
139 resultsURLsByTypeAndKind[results.resultType(url)][results.resultKind
(url)] = url; | |
140 }); | |
141 | |
142 $.each(resultsURLsByTypeAndKind, function(resultType, resultsURLsByKind)
{ | |
143 if ($.isEmptyObject(resultsURLsByKind)) | |
144 return; | |
145 if (results.kUnknownKind in resultsURLsByKind) { | |
146 // This is something like "crash" that isn't a comparison. | |
147 this.addRow(resultType, resultsURLsByKind[results.kUnknownKind])
; | |
148 return; | |
149 } | |
150 this.addComparison(resultType, resultsURLsByKind); | |
151 }.bind(this)); | |
152 | |
153 if (!this.children.length) | |
154 this.textContent = 'No results to display.' | |
155 } | |
156 }); | |
157 | |
158 ui.results.ResultsDetails = base.extends('div', { | |
159 init: function(delegate, failureInfo) | |
160 { | |
161 this.className = 'results-detail'; | |
162 this._delegate = delegate; | |
163 this._failureInfo = failureInfo; | |
164 this._haveShownOnce = false; | |
165 }, | |
166 show: function() { | |
167 if (this._haveShownOnce) | |
168 return; | |
169 this._haveShownOnce = true; | |
170 this._delegate.fetchResultsURLs(this._failureInfo, function(resultsURLs)
{ | |
171 var resultsGrid = new ui.results.ResultsGrid(); | |
172 resultsGrid.addResults(resultsURLs); | |
173 | |
174 $(this).empty().append( | |
175 new ui.actions.List([ | |
176 new ui.actions.Previous(), | |
177 new ui.actions.Next() | |
178 ])).append(resultsGrid); | |
179 | |
180 | |
181 }.bind(this)); | |
182 }, | |
183 }); | |
184 | |
185 function isAnyReftest(testName, resultsByTest) | |
186 { | |
187 return Object.keys(resultsByTest[testName]).map(function(builder) { | |
188 return resultsByTest[testName][builder]; | |
189 }).some(function(resultNode) { | |
190 return resultNode.reftest_type && resultNode.reftest_type.length; | |
191 }); | |
192 } | |
193 | |
194 ui.results.FlakinessData = base.extends('iframe', { | |
195 init: function() | |
196 { | |
197 this.className = 'flakiness-iframe'; | |
198 this.src = ui.urlForEmbeddedFlakinessDashboard(); | |
199 this.addEventListener('load', function() { | |
200 window.addEventListener('message', this._handleMessage.bind(this)); | |
201 }); | |
202 }, | |
203 _handleMessage: function(event) { | |
204 if (!this.contentWindow) | |
205 return; | |
206 | |
207 // Check for null event.origin so that the unittests can get past this p
oint. | |
208 // FIXME: Is this safe? In practice, there's no meaningful harm that can
come from | |
209 // a malicious page sending us heightChanged commands, so it doesn't rea
lly matter. | |
210 if (event.origin !== 'null' && event.origin != 'http://test-results.apps
pot.com') { | |
211 console.log('Invalid origin: ' + event.origin); | |
212 return; | |
213 } | |
214 | |
215 if (event.data.command != 'heightChanged') { | |
216 console.log('Unknown postMessage command: ' + event.data); | |
217 return; | |
218 } | |
219 | |
220 this.style.height = event.data.height + 'px'; | |
221 } | |
222 }); | |
223 | |
224 ui.results.TestSelector = base.extends('div', { | |
225 init: function(delegate, resultsByTest) | |
226 { | |
227 this.className = 'test-selector'; | |
228 this._delegate = delegate; | |
229 | |
230 var topPanel = document.createElement('div'); | |
231 topPanel.className = 'top-panel'; | |
232 this.appendChild(topPanel); | |
233 | |
234 this._appendResizeHandle(); | |
235 | |
236 var bottomPanel = document.createElement('div'); | |
237 bottomPanel.className = 'bottom-panel'; | |
238 this.appendChild(bottomPanel); | |
239 | |
240 this._flakinessData = new ui.results.FlakinessData(); | |
241 this.appendChild(this._flakinessData); | |
242 | |
243 var testNames = Object.keys(resultsByTest); | |
244 testNames.sort().forEach(function(testName) { | |
245 var nonLinkTitle = document.createElement('a'); | |
246 nonLinkTitle.classList.add('non-link-title'); | |
247 nonLinkTitle.textContent = testName; | |
248 | |
249 var linkTitle = document.createElement('a'); | |
250 linkTitle.classList.add('link-title'); | |
251 linkTitle.setAttribute('href', ui.urlForFlakinessDashboard([testName
])) | |
252 linkTitle.textContent = testName; | |
253 | |
254 var header = document.createElement('h3'); | |
255 header.appendChild(nonLinkTitle); | |
256 header.appendChild(linkTitle); | |
257 header.addEventListener('click', this._showResults.bind(this, header
, false)); | |
258 topPanel.appendChild(header); | |
259 }, this); | |
260 | |
261 // If we have a small amount of content, don't show the resize handler. | |
262 // Otherwise, set the minHeight so that the percentage height of the | |
263 // topPanel is not too small. | |
264 if (testNames.length <= 4) | |
265 this.removeChild(this.querySelector('.resize-handle')); | |
266 else | |
267 topPanel.style.minHeight = '100px'; | |
268 }, | |
269 _appendResizeHandle: function() | |
270 { | |
271 var resizeHandle = document.createElement('div'); | |
272 resizeHandle.className = 'resize-handle'; | |
273 this.appendChild(resizeHandle); | |
274 | |
275 resizeHandle.addEventListener('mousedown', function(event) { | |
276 this._is_resizing = true; | |
277 event.preventDefault(); | |
278 }.bind(this)); | |
279 | |
280 var cancelResize = function(event) { this._is_resizing = false; }.bind(t
his); | |
281 this.addEventListener('mouseup', cancelResize); | |
282 // FIXME: Use addEventListener once WebKit adds support for mouseleave/m
ouseenter. | |
283 $(window).bind('mouseleave', cancelResize); | |
284 | |
285 this.addEventListener('mousemove', function(event) { | |
286 if (!this._is_resizing) | |
287 return; | |
288 var mouseY = event.clientY + document.body.scrollTop - this.offsetTo
p; | |
289 var percentage = 100 * mouseY / this.offsetHeight; | |
290 document.querySelector('.top-panel').style.maxHeight = percentage +
'%'; | |
291 }.bind(this)) | |
292 }, | |
293 _showResults: function(header, scrollInfoView) | |
294 { | |
295 if (!header) | |
296 return false; | |
297 | |
298 var activeHeader = this.querySelector('.active') | |
299 if (activeHeader) | |
300 activeHeader.classList.remove('active'); | |
301 header.classList.add('active'); | |
302 | |
303 var testName = this.currentTestName(); | |
304 this._flakinessData.src = ui.urlForEmbeddedFlakinessDashboard([testName]
); | |
305 | |
306 var bottomPanel = this.querySelector('.bottom-panel') | |
307 bottomPanel.innerHTML = ''; | |
308 bottomPanel.appendChild(this._delegate.contentForTest(testName)); | |
309 | |
310 var topPanel = this.querySelector('.top-panel'); | |
311 if (scrollInfoView) { | |
312 topPanel.scrollTop = header.offsetTop; | |
313 if (header.offsetTop - topPanel.scrollTop < header.offsetHeight) | |
314 topPanel.scrollTop = topPanel.scrollTop - header.offsetHeight; | |
315 } | |
316 | |
317 var resultsDetails = this.querySelectorAll('.results-detail'); | |
318 if (resultsDetails.length) | |
319 resultsDetails[0].show(); | |
320 setTimeout(function() { | |
321 Array.prototype.forEach.call(resultsDetails, function(resultsDetail)
{ | |
322 resultsDetail.show(); | |
323 }); | |
324 }, kResultsPrefetchDelayMS); | |
325 | |
326 return true; | |
327 }, | |
328 nextResult: function() | |
329 { | |
330 if (this.querySelector('.builder-selector').nextResult()) | |
331 return true; | |
332 return this.nextTest(); | |
333 }, | |
334 previousResult: function() | |
335 { | |
336 if (this.querySelector('.builder-selector').previousResult()) | |
337 return true; | |
338 return this.previousTest(); | |
339 }, | |
340 nextTest: function() | |
341 { | |
342 return this._showResults(this.querySelector('.active').nextSibling, true
); | |
343 }, | |
344 previousTest: function() | |
345 { | |
346 var succeeded = this._showResults(this.querySelector('.active').previous
Sibling, true); | |
347 if (succeeded) | |
348 this.querySelector('.builder-selector').lastResult(); | |
349 return succeeded; | |
350 }, | |
351 firstResult: function() | |
352 { | |
353 this._showResults(this.querySelector('h3'), true); | |
354 }, | |
355 currentTestName: function() | |
356 { | |
357 return this.querySelector('.active .non-link-title').textContent; | |
358 } | |
359 }); | |
360 | |
361 ui.results.BuilderSelector = base.extends('div', { | |
362 init: function(delegate, testName, resultsByBuilder) | |
363 { | |
364 this.className = 'builder-selector'; | |
365 this._delegate = delegate; | |
366 | |
367 var tabStrip = this.appendChild(document.createElement('ul')); | |
368 | |
369 Object.keys(resultsByBuilder).sort().forEach(function(builderName) { | |
370 var builderHash = base.underscoredBuilderName(builderName); | |
371 | |
372 var link = document.createElement('a'); | |
373 $(link).attr('href', "#" + builderHash).text(ui.displayNameForBuilde
r(builderName)); | |
374 tabStrip.appendChild(document.createElement('li')).appendChild(link)
; | |
375 | |
376 var content = this._delegate.contentForTestAndBuilder(testName, buil
derName); | |
377 content.id = builderHash; | |
378 this.appendChild(content); | |
379 }, this); | |
380 | |
381 $(this).tabs(); | |
382 }, | |
383 nextResult: function() | |
384 { | |
385 var nextIndex = $(this).tabs('option', 'selected') + 1; | |
386 if (nextIndex >= $(this).tabs('length')) | |
387 return false | |
388 $(this).tabs('option', 'selected', nextIndex); | |
389 return true; | |
390 }, | |
391 previousResult: function() | |
392 { | |
393 var previousIndex = $(this).tabs('option', 'selected') - 1; | |
394 if (previousIndex < 0) | |
395 return false; | |
396 $(this).tabs('option', 'selected', previousIndex); | |
397 return true; | |
398 }, | |
399 firstResult: function() | |
400 { | |
401 $(this).tabs('option', 'selected', 0); | |
402 }, | |
403 lastResult: function() | |
404 { | |
405 $(this).tabs('option', 'selected', $(this).tabs('length') - 1); | |
406 } | |
407 }); | |
408 | |
409 ui.results.View = base.extends('div', { | |
410 init: function(delegate) | |
411 { | |
412 this.className = 'results-view'; | |
413 this._delegate = delegate; | |
414 }, | |
415 contentForTest: function(testName) | |
416 { | |
417 var rebaselineAction; | |
418 if (isAnyReftest(testName, this._resultsByTest)) | |
419 rebaselineAction = $('<div class="non-action-button">Reftests cannot
be rebaselined. Email webkit-gardening@chromium.org if unsure how to fix this.<
/div>'); | |
420 else | |
421 rebaselineAction = new ui.actions.List([new ui.actions.Rebaseline().
makeDefault()]); | |
422 $(rebaselineAction).addClass('rebaseline-action'); | |
423 | |
424 var builderSelector = new ui.results.BuilderSelector(this, testName, thi
s._resultsByTest[testName]); | |
425 $(builderSelector).append(rebaselineAction).append($('<br style="clear:b
oth">')); | |
426 $(builderSelector).bind('tabsselect', function(event, ui) { | |
427 // We will probably have pre-fetched the tab already, but we need to
make sure. | |
428 ui.panel.show(); | |
429 }); | |
430 return builderSelector; | |
431 }, | |
432 contentForTestAndBuilder: function(testName, builderName) | |
433 { | |
434 var failureInfo = results.failureInfoForTestAndBuilder(this._resultsByTe
st, testName, builderName); | |
435 return new ui.results.ResultsDetails(this, failureInfo); | |
436 }, | |
437 setResultsByTest: function(resultsByTest) | |
438 { | |
439 $(this).empty(); | |
440 this._resultsByTest = resultsByTest; | |
441 this._testSelector = new ui.results.TestSelector(this, resultsByTest); | |
442 this.appendChild(this._testSelector); | |
443 }, | |
444 fetchResultsURLs: function(failureInfo, callback) | |
445 { | |
446 this._delegate.fetchResultsURLs(failureInfo, callback) | |
447 }, | |
448 nextResult: function() | |
449 { | |
450 return this._testSelector.nextResult(); | |
451 }, | |
452 previousResult: function() | |
453 { | |
454 return this._testSelector.previousResult(); | |
455 }, | |
456 nextTest: function() | |
457 { | |
458 return this._testSelector.nextTest(); | |
459 }, | |
460 previousTest: function() | |
461 { | |
462 return this._testSelector.previousTest(); | |
463 }, | |
464 firstResult: function() | |
465 { | |
466 this._testSelector.firstResult() | |
467 }, | |
468 currentTestName: function() | |
469 { | |
470 return this._testSelector.currentTestName() | |
471 } | |
472 }); | |
473 | |
474 })(); | |
OLD | NEW |