1/**
2 * Copyright (c) 2017 Google Inc. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you
5 * may not use this file except in compliance with the License. You may
6 * obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13 * implied. See the License for the specific language governing
14 * permissions and limitations under the License.
15 */
16
17(function($, moment) {
18
19/**
20 * Display the log links in a modal window.
21 * @param linkList A list of [name, url] tuples representing log links.
22 */
23function showLinks(container, linkList) {
24  if (!linkList || linkList.length == 0) return;
25
26  var logCollection = $('<ul class="collection"></ul>');
27  var entries = linkList.reduce(function(acc, entry) {
28    if (!entry || entry.length == 0) return acc;
29    var link = '<a href="' + entry[1] + '"';
30    link += 'class="collection-item">' + entry[0] + '</li>';
31    return acc + link;
32  }, '');
33  logCollection.html(entries);
34
35  if (container.find('#info-modal').length == 0) {
36    var modal =
37        $('<div id="info-modal" class="modal modal-fixed-footer"></div>');
38    var content = $('<div class="modal-content"></div>');
39    content.append('<h4>Links</h4>');
40    content.append('<div class="info-container"></div>');
41    content.appendTo(modal);
42    var footer = $('<div class="modal-footer"></div>');
43    footer.append('<a class="btn-flat modal-close">Close</a></div>');
44    footer.appendTo(modal);
45    modal.appendTo(container);
46  }
47  var infoContainer = $('#info-modal>.modal-content>.info-container');
48  infoContainer.empty();
49  logCollection.appendTo(infoContainer);
50  $('#info-modal').modal({dismissible: true});
51  $('#info-modal').modal('open');
52}
53
54/**
55 * Get the nickname for a test case result.
56 *
57 * Removes the result prefix and suffix, extracting only the result name.
58 *
59 * @param testCaseResult The string name of a VtsReportMessage.TestCaseResult.
60 * @returns the string nickname of the result.
61 */
62function getNickname(testCaseResult) {
63  return testCaseResult.replace('TEST_CASE_RESULT_', '')
64      .replace('_RESULT', '')
65      .trim()
66      .toLowerCase();
67}
68
69/**
70 * Get the badge color from ratio value.
71 *
72 * @param the percentage value.
73 * @returns the string of color for the badge.
74 */
75function getBadgeColor(ratio) {
76  var color = "orange";
77  if (ratio <= 20) {
78    color = "red";
79  } else if (ratio >= 70) {
80    color = "green";
81  }
82  return color;
83}
84
85/**
86 * Get the rounded value.
87 *
88 * @param the percentage value.
89 * @returns the rounded value from percentage value.
90 */
91function getRoundValue(ratio) {
92    return Math.round(ratio * 1000) / 10;
93}
94
95/**
96 * Display test data in the body beneath a test run's metadata.
97 * @param container The jquery object in which to insert the test metadata.
98 * @param data The json object containing the columns to display.
99 * @param lineHeight The height of each list element.
100 */
101function displayTestDetails(container, data, lineHeight) {
102  var nCol = data.length;
103  var width = 's' + (12 / nCol);
104  test = container;
105  var maxLines = 0;
106  data.forEach(function(column, index) {
107    if (column.data == undefined || column.name == undefined) {
108      return;
109    }
110    var classes = 'col test-col grey lighten-5 ' + width;
111    if (index != nCol - 1) {
112      classes += ' bordered';
113    }
114    if (index == 0) {
115      classes += ' left-most';
116    }
117    if (index == nCol - 1) {
118      classes += ' right-most';
119    }
120    var colContainer = $('<div class="' + classes + '"></div>');
121    var col = $('<div class="test-case-container"></div>');
122    colContainer.appendTo(container);
123    var count = column.data.length;
124    var head = $('<h5 class="test-result-label white"></h5>')
125                   .text(getNickname(column.name))
126                   .appendTo(colContainer)
127                   .css('text-transform', 'capitalize');
128    $('<div class="indicator right center"></div>')
129        .text(count)
130        .addClass(column.name)
131        .appendTo(head);
132    col.appendTo(colContainer);
133    var list = $('<ul></ul>').appendTo(col);
134    column.data.forEach(function(testCase) {
135      $('<li></li>')
136          .text(testCase)
137          .addClass('test-case')
138          .css('font-size', lineHeight - 2)
139          .css('line-height', lineHeight + 'px')
140          .appendTo(list);
141    });
142    if (count > maxLines) {
143      maxLines = count;
144    }
145  });
146  var containers = container.find('.test-case-container');
147  containers.height(maxLines * lineHeight);
148}
149
150/**
151 * Click handler for displaying test run details.
152 * @param e The click event.
153 */
154function testRunClick(e) {
155  var header = $(this);
156  var icon = header.find('.material-icons.expand-arrow');
157  var container = header.parent().find('.test-results');
158  var test = header.attr('test');
159  var time = header.attr('time');
160  var url = '/api/test_run?test=' + test + '&timestamp=' + time;
161  if (header.parent().hasClass('active')) {
162    header.parent().removeClass('active');
163    header.removeClass('active');
164    icon.removeClass('rotate');
165    header.siblings('.collapsible-body').stop(true, false).slideUp({
166      duration: 100,
167      easing: 'easeOutQuart',
168      queue: false,
169      complete: function() {
170        header.css('height', '');
171      }
172    });
173  } else {
174    container.empty();
175    header.parent().addClass('active');
176    header.addClass('active');
177    header.addClass('disabled');
178    icon.addClass('rotate');
179    $.get(url)
180        .done(function(data) {
181          displayTestDetails(container, data, 16);
182          header.siblings('.collapsible-body').stop(true, false).slideDown({
183            duration: 100,
184            easing: 'easeOutQuart',
185            queue: false,
186            complete: function() {
187              header.css('height', '');
188            }
189          });
190        })
191        .fail(function() {
192          icon.removeClass('rotate');
193        })
194        .always(function() {
195          header.removeClass('disabled');
196        });
197  }
198}
199
200/**
201 * Append a clickable indicator link to the container.
202 * @param container The jquery object to append the indicator to.
203 * @param content The text to display in the indicator.
204 * @param classes Additional space-delimited classes to add to the indicator.
205 * @param click The click handler to assign to the indicator.
206 * @returns The jquery object for the indicator.
207 */
208function createClickableIndicator(container, content, classes, click) {
209  var link = $('<span></span>');
210  link.addClass('indicator badge padded hoverable waves-effect');
211  link.addClass(classes);
212  link.css('color', 'white');
213  link.css('margin-left', '1px');
214  link.append(content);
215  link.appendTo(container);
216  link.click(click);
217  return link;
218}
219
220function displayTestMetadata(container, metadataList, showTestNames = false) {
221  var popout = $('<ul></ul>');
222  popout.attr('data-collapsible', 'expandable');
223  popout.addClass('collapsible popout test-runs');
224  popout.appendTo(container);
225  popout.unbind();
226  metadataList.forEach(function(metadata) {
227    var li = $('<li class="test-run-container"></li>');
228    li.appendTo(popout);
229    var div = $('<div></div>');
230    var test = metadata.testRun.testName;
231    var startTime = metadata.testRun.startTimestamp;
232    var endTime = metadata.testRun.endTimestamp;
233    div.attr('test', test);
234    div.attr('time', startTime);
235    div.addClass('collapsible-header test-run');
236    div.appendTo(li);
237    div.unbind().click(testRunClick);
238    var span = $('<span></span>');
239    span.addClass('test-run-metadata');
240    span.appendTo(div);
241    span.click(function() {
242      return false;
243    });
244    if (showTestNames) {
245      $('<span class="test-run-label"></span>').text(test).appendTo(span);
246      span.append('<br>');
247    }
248    if (metadata.deviceInfo) {
249      $('<b></b>').text(metadata.deviceInfo).appendTo(span);
250      span.append('<br>');
251    }
252    if (metadata.abiInfo) {
253      $('<b></b>').text('ABI: ').appendTo(span)
254      span.append(metadata.abiInfo).append('<br>');
255    }
256    $('<b></b>').text('VTS Build: ').appendTo(span)
257    span.append(metadata.testRun.testBuildId).append('<br>');
258    $('<b></b>').text('Host: ').appendTo(span)
259    span.append(metadata.testRun.hostName).append('<br>');
260    var timeString =
261        (moment().renderTime(startTime, false) + ' - ' +
262         moment().renderTime(endTime, true) + ' (' +
263         moment().renderDuration(endTime - startTime) + ')');
264    span.append(timeString);
265    var indicator = $('<span></span>');
266    var color = metadata.testRun.failCount > 0 ? 'red' : 'green';
267    indicator.addClass('indicator badge ' + color);
268    indicator.css('color', 'white');
269    indicator.append(
270        metadata.testRun.passCount + '/' +
271        (metadata.testRun.passCount + metadata.testRun.failCount));
272    indicator.appendTo(div);
273    if (metadata.testRun.coveredLineCount != undefined &&
274        metadata.testRun.totalLineCount != undefined) {
275      var url = ('/show_coverage?testName=' + test + '&startTime=' + startTime);
276      var covered = metadata.testRun.coveredLineCount;
277      var total = metadata.testRun.totalLineCount;
278      var covPct = getRoundValue(covered / total);
279      var color = getBadgeColor(covPct);
280      var coverage =
281          ('Coverage: ' + covered + '/' + total + ' (' + covPct + '%)');
282      createClickableIndicator(div, coverage, color, function(evt) {
283        window.location.href = url;
284        return false;
285      });
286    }
287    if (metadata.testRun.coveredApiCount != undefined &&
288        metadata.testRun.totalApiCount != undefined) {
289      var covered = metadata.testRun.coveredApiCount;
290      var total = metadata.testRun.totalApiCount;
291      var covPct = getRoundValue(covered / total);
292      var color = getBadgeColor(covPct);
293      var apiCoverage = ('API Coverage: ' + covered + '/' + total + ' (' + covPct + '%)');
294      createClickableIndicator(div, apiCoverage, color, function(evt) {
295        $('#apiCoverageModal')
296            .data('urlSafeKeyList', metadata.testRun.apiCoverageKeyList);
297        $('#apiCoverageModal').modal('open');
298        return false;
299      });
300    }
301    if (metadata.testRun.logLinks != undefined) {
302      createClickableIndicator(div, 'Links', 'grey lighten-1', function() {
303        showLinks(popout, metadata.testRun.logLinks);
304        return false;
305      });
306    }
307    if ($('#coverageModalGraph').length) {
308      createClickableIndicator(div, 'Graph', 'grey lighten-1', function(evt) {
309        $('#coverageModalGraph').data('testname', test);
310        $('#coverageModalGraph').modal('open');
311        $(evt.target).removeClass('grey');
312        $(evt.target).addClass('blue');
313        return false;
314      });
315    }
316
317    var expand = $('<i></i>');
318    expand.addClass('material-icons expand-arrow')
319    expand.text('expand_more');
320    expand.appendTo(div);
321    var body = $('<div></div>')
322                   .addClass('collapsible-body test-results row')
323                   .appendTo(li);
324    if (metadata.testDetails != undefined) {
325      expand.addClass('rotate');
326      li.addClass('active');
327      div.addClass('active');
328      displayTestDetails(body, metadata.testDetails, 16);
329      div.siblings('.collapsible-body').stop(true, false).slideDown({
330        duration: 0,
331        queue: false,
332        complete: function() {
333          div.css('height', '');
334        }
335      });
336    }
337  });
338}
339
340/**
341 * Display test metadata in a vertical popout.
342 * @param container The jquery object in which to insert the test metadata.
343 * @param metadataList The list of metadata objects to render on the display.
344 * @param showTestNames True to label each entry with the test module name.
345 */
346$.fn.showTests = function(metadataList, showTestNames = false) {
347  displayTestMetadata($(this), metadataList, showTestNames);
348}
349})(jQuery, moment);
350