1// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 3 4// Coverage.py HTML report browser code. 5/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ 6/*global coverage: true, document, window, $ */ 7 8coverage = {}; 9 10// Find all the elements with shortkey_* class, and use them to assign a shortcut key. 11coverage.assign_shortkeys = function () { 12 $("*[class*='shortkey_']").each(function (i, e) { 13 $.each($(e).attr("class").split(" "), function (i, c) { 14 if (/^shortkey_/.test(c)) { 15 $(document).bind('keydown', c.substr(9), function () { 16 $(e).click(); 17 }); 18 } 19 }); 20 }); 21}; 22 23// Create the events for the help panel. 24coverage.wire_up_help_panel = function () { 25 $("#keyboard_icon").click(function () { 26 // Show the help panel, and position it so the keyboard icon in the 27 // panel is in the same place as the keyboard icon in the header. 28 $(".help_panel").show(); 29 var koff = $("#keyboard_icon").offset(); 30 var poff = $("#panel_icon").position(); 31 $(".help_panel").offset({ 32 top: koff.top-poff.top, 33 left: koff.left-poff.left 34 }); 35 }); 36 $("#panel_icon").click(function () { 37 $(".help_panel").hide(); 38 }); 39}; 40 41// Create the events for the filter box. 42coverage.wire_up_filter = function () { 43 // Cache elements. 44 var table = $("table.index"); 45 var table_rows = table.find("tbody tr"); 46 var table_row_names = table_rows.find("td.name a"); 47 var no_rows = $("#no_rows"); 48 49 // Create a duplicate table footer that we can modify with dynamic summed values. 50 var table_footer = $("table.index tfoot tr"); 51 var table_dynamic_footer = table_footer.clone(); 52 table_dynamic_footer.attr('class', 'total_dynamic hidden'); 53 table_footer.after(table_dynamic_footer); 54 55 // Observe filter keyevents. 56 $("#filter").on("keyup change", $.debounce(150, function (event) { 57 var filter_value = $(this).val(); 58 59 if (filter_value === "") { 60 // Filter box is empty, remove all filtering. 61 table_rows.removeClass("hidden"); 62 63 // Show standard footer, hide dynamic footer. 64 table_footer.removeClass("hidden"); 65 table_dynamic_footer.addClass("hidden"); 66 67 // Hide placeholder, show table. 68 if (no_rows.length > 0) { 69 no_rows.hide(); 70 } 71 table.show(); 72 73 } 74 else { 75 // Filter table items by value. 76 var hide = $([]); 77 var show = $([]); 78 79 // Compile elements to hide / show. 80 $.each(table_row_names, function () { 81 var element = $(this).parents("tr"); 82 83 if ($(this).text().indexOf(filter_value) === -1) { 84 // hide 85 hide = hide.add(element); 86 } 87 else { 88 // show 89 show = show.add(element); 90 } 91 }); 92 93 // Perform DOM manipulation. 94 hide.addClass("hidden"); 95 show.removeClass("hidden"); 96 97 // Show placeholder if no rows will be displayed. 98 if (no_rows.length > 0) { 99 if (show.length === 0) { 100 // Show placeholder, hide table. 101 no_rows.show(); 102 table.hide(); 103 } 104 else { 105 // Hide placeholder, show table. 106 no_rows.hide(); 107 table.show(); 108 } 109 } 110 111 // Manage dynamic header: 112 if (hide.length > 0) { 113 // Calculate new dynamic sum values based on visible rows. 114 for (var column = 2; column < 20; column++) { 115 // Calculate summed value. 116 var cells = table_rows.find('td:nth-child(' + column + ')'); 117 if (!cells.length) { 118 // No more columns...! 119 break; 120 } 121 122 var sum = 0, numer = 0, denom = 0; 123 $.each(cells.filter(':visible'), function () { 124 var ratio = $(this).data("ratio"); 125 if (ratio) { 126 var splitted = ratio.split(" "); 127 numer += parseInt(splitted[0], 10); 128 denom += parseInt(splitted[1], 10); 129 } 130 else { 131 sum += parseInt(this.innerHTML, 10); 132 } 133 }); 134 135 // Get footer cell element. 136 var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); 137 138 // Set value into dynamic footer cell element. 139 if (cells[0].innerHTML.indexOf('%') > -1) { 140 // Percentage columns use the numerator and denominator, 141 // and adapt to the number of decimal places. 142 var match = /\.([0-9]+)/.exec(cells[0].innerHTML); 143 var places = 0; 144 if (match) { 145 places = match[1].length; 146 } 147 var pct = numer * 100 / denom; 148 footer_cell.text(pct.toFixed(places) + '%'); 149 } 150 else { 151 footer_cell.text(sum); 152 } 153 } 154 155 // Hide standard footer, show dynamic footer. 156 table_footer.addClass("hidden"); 157 table_dynamic_footer.removeClass("hidden"); 158 } 159 else { 160 // Show standard footer, hide dynamic footer. 161 table_footer.removeClass("hidden"); 162 table_dynamic_footer.addClass("hidden"); 163 } 164 } 165 })); 166 167 // Trigger change event on setup, to force filter on page refresh 168 // (filter value may still be present). 169 $("#filter").trigger("change"); 170}; 171 172// Loaded on index.html 173coverage.index_ready = function ($) { 174 // Look for a cookie containing previous sort settings: 175 var sort_list = []; 176 var cookie_name = "COVERAGE_INDEX_SORT"; 177 var i; 178 179 // This almost makes it worth installing the jQuery cookie plugin: 180 if (document.cookie.indexOf(cookie_name) > -1) { 181 var cookies = document.cookie.split(";"); 182 for (i = 0; i < cookies.length; i++) { 183 var parts = cookies[i].split("="); 184 185 if ($.trim(parts[0]) === cookie_name && parts[1]) { 186 sort_list = eval("[[" + parts[1] + "]]"); 187 break; 188 } 189 } 190 } 191 192 // Create a new widget which exists only to save and restore 193 // the sort order: 194 $.tablesorter.addWidget({ 195 id: "persistentSort", 196 197 // Format is called by the widget before displaying: 198 format: function (table) { 199 if (table.config.sortList.length === 0 && sort_list.length > 0) { 200 // This table hasn't been sorted before - we'll use 201 // our stored settings: 202 $(table).trigger('sorton', [sort_list]); 203 } 204 else { 205 // This is not the first load - something has 206 // already defined sorting so we'll just update 207 // our stored value to match: 208 sort_list = table.config.sortList; 209 } 210 } 211 }); 212 213 // Configure our tablesorter to handle the variable number of 214 // columns produced depending on report options: 215 var headers = []; 216 var col_count = $("table.index > thead > tr > th").length; 217 218 headers[0] = { sorter: 'text' }; 219 for (i = 1; i < col_count-1; i++) { 220 headers[i] = { sorter: 'digit' }; 221 } 222 headers[col_count-1] = { sorter: 'percent' }; 223 224 // Enable the table sorter: 225 $("table.index").tablesorter({ 226 widgets: ['persistentSort'], 227 headers: headers 228 }); 229 230 coverage.assign_shortkeys(); 231 coverage.wire_up_help_panel(); 232 coverage.wire_up_filter(); 233 234 // Watch for page unload events so we can save the final sort settings: 235 $(window).unload(function () { 236 document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; 237 }); 238}; 239 240// -- pyfile stuff -- 241 242coverage.pyfile_ready = function ($) { 243 // If we're directed to a particular line number, highlight the line. 244 var frag = location.hash; 245 if (frag.length > 2 && frag[1] === 'n') { 246 $(frag).addClass('highlight'); 247 coverage.set_sel(parseInt(frag.substr(2), 10)); 248 } 249 else { 250 coverage.set_sel(0); 251 } 252 253 $(document) 254 .bind('keydown', 'j', coverage.to_next_chunk_nicely) 255 .bind('keydown', 'k', coverage.to_prev_chunk_nicely) 256 .bind('keydown', '0', coverage.to_top) 257 .bind('keydown', '1', coverage.to_first_chunk) 258 ; 259 260 $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); 261 $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); 262 $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); 263 $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); 264 265 coverage.assign_shortkeys(); 266 coverage.wire_up_help_panel(); 267}; 268 269coverage.toggle_lines = function (btn, cls) { 270 btn = $(btn); 271 var hide = "hide_"+cls; 272 if (btn.hasClass(hide)) { 273 $("#source ."+cls).removeClass(hide); 274 btn.removeClass(hide); 275 } 276 else { 277 $("#source ."+cls).addClass(hide); 278 btn.addClass(hide); 279 } 280}; 281 282// Return the nth line div. 283coverage.line_elt = function (n) { 284 return $("#t" + n); 285}; 286 287// Return the nth line number div. 288coverage.num_elt = function (n) { 289 return $("#n" + n); 290}; 291 292// Return the container of all the code. 293coverage.code_container = function () { 294 return $(".linenos"); 295}; 296 297// Set the selection. b and e are line numbers. 298coverage.set_sel = function (b, e) { 299 // The first line selected. 300 coverage.sel_begin = b; 301 // The next line not selected. 302 coverage.sel_end = (e === undefined) ? b+1 : e; 303}; 304 305coverage.to_top = function () { 306 coverage.set_sel(0, 1); 307 coverage.scroll_window(0); 308}; 309 310coverage.to_first_chunk = function () { 311 coverage.set_sel(0, 1); 312 coverage.to_next_chunk(); 313}; 314 315coverage.is_transparent = function (color) { 316 // Different browsers return different colors for "none". 317 return color === "transparent" || color === "rgba(0, 0, 0, 0)"; 318}; 319 320coverage.to_next_chunk = function () { 321 var c = coverage; 322 323 // Find the start of the next colored chunk. 324 var probe = c.sel_end; 325 var color, probe_line; 326 while (true) { 327 probe_line = c.line_elt(probe); 328 if (probe_line.length === 0) { 329 return; 330 } 331 color = probe_line.css("background-color"); 332 if (!c.is_transparent(color)) { 333 break; 334 } 335 probe++; 336 } 337 338 // There's a next chunk, `probe` points to it. 339 var begin = probe; 340 341 // Find the end of this chunk. 342 var next_color = color; 343 while (next_color === color) { 344 probe++; 345 probe_line = c.line_elt(probe); 346 next_color = probe_line.css("background-color"); 347 } 348 c.set_sel(begin, probe); 349 c.show_selection(); 350}; 351 352coverage.to_prev_chunk = function () { 353 var c = coverage; 354 355 // Find the end of the prev colored chunk. 356 var probe = c.sel_begin-1; 357 var probe_line = c.line_elt(probe); 358 if (probe_line.length === 0) { 359 return; 360 } 361 var color = probe_line.css("background-color"); 362 while (probe > 0 && c.is_transparent(color)) { 363 probe--; 364 probe_line = c.line_elt(probe); 365 if (probe_line.length === 0) { 366 return; 367 } 368 color = probe_line.css("background-color"); 369 } 370 371 // There's a prev chunk, `probe` points to its last line. 372 var end = probe+1; 373 374 // Find the beginning of this chunk. 375 var prev_color = color; 376 while (prev_color === color) { 377 probe--; 378 probe_line = c.line_elt(probe); 379 prev_color = probe_line.css("background-color"); 380 } 381 c.set_sel(probe+1, end); 382 c.show_selection(); 383}; 384 385// Return the line number of the line nearest pixel position pos 386coverage.line_at_pos = function (pos) { 387 var l1 = coverage.line_elt(1), 388 l2 = coverage.line_elt(2), 389 result; 390 if (l1.length && l2.length) { 391 var l1_top = l1.offset().top, 392 line_height = l2.offset().top - l1_top, 393 nlines = (pos - l1_top) / line_height; 394 if (nlines < 1) { 395 result = 1; 396 } 397 else { 398 result = Math.ceil(nlines); 399 } 400 } 401 else { 402 result = 1; 403 } 404 return result; 405}; 406 407// Returns 0, 1, or 2: how many of the two ends of the selection are on 408// the screen right now? 409coverage.selection_ends_on_screen = function () { 410 if (coverage.sel_begin === 0) { 411 return 0; 412 } 413 414 var top = coverage.line_elt(coverage.sel_begin); 415 var next = coverage.line_elt(coverage.sel_end-1); 416 417 return ( 418 (top.isOnScreen() ? 1 : 0) + 419 (next.isOnScreen() ? 1 : 0) 420 ); 421}; 422 423coverage.to_next_chunk_nicely = function () { 424 coverage.finish_scrolling(); 425 if (coverage.selection_ends_on_screen() === 0) { 426 // The selection is entirely off the screen: select the top line on 427 // the screen. 428 var win = $(window); 429 coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); 430 } 431 coverage.to_next_chunk(); 432}; 433 434coverage.to_prev_chunk_nicely = function () { 435 coverage.finish_scrolling(); 436 if (coverage.selection_ends_on_screen() === 0) { 437 var win = $(window); 438 coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); 439 } 440 coverage.to_prev_chunk(); 441}; 442 443// Select line number lineno, or if it is in a colored chunk, select the 444// entire chunk 445coverage.select_line_or_chunk = function (lineno) { 446 var c = coverage; 447 var probe_line = c.line_elt(lineno); 448 if (probe_line.length === 0) { 449 return; 450 } 451 var the_color = probe_line.css("background-color"); 452 if (!c.is_transparent(the_color)) { 453 // The line is in a highlighted chunk. 454 // Search backward for the first line. 455 var probe = lineno; 456 var color = the_color; 457 while (probe > 0 && color === the_color) { 458 probe--; 459 probe_line = c.line_elt(probe); 460 if (probe_line.length === 0) { 461 break; 462 } 463 color = probe_line.css("background-color"); 464 } 465 var begin = probe + 1; 466 467 // Search forward for the last line. 468 probe = lineno; 469 color = the_color; 470 while (color === the_color) { 471 probe++; 472 probe_line = c.line_elt(probe); 473 color = probe_line.css("background-color"); 474 } 475 476 coverage.set_sel(begin, probe); 477 } 478 else { 479 coverage.set_sel(lineno); 480 } 481}; 482 483coverage.show_selection = function () { 484 var c = coverage; 485 486 // Highlight the lines in the chunk 487 c.code_container().find(".highlight").removeClass("highlight"); 488 for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { 489 c.num_elt(probe).addClass("highlight"); 490 } 491 492 c.scroll_to_selection(); 493}; 494 495coverage.scroll_to_selection = function () { 496 // Scroll the page if the chunk isn't fully visible. 497 if (coverage.selection_ends_on_screen() < 2) { 498 // Need to move the page. The html,body trick makes it scroll in all 499 // browsers, got it from http://stackoverflow.com/questions/3042651 500 var top = coverage.line_elt(coverage.sel_begin); 501 var top_pos = parseInt(top.offset().top, 10); 502 coverage.scroll_window(top_pos - 30); 503 } 504}; 505 506coverage.scroll_window = function (to_pos) { 507 $("html,body").animate({scrollTop: to_pos}, 200); 508}; 509 510coverage.finish_scrolling = function () { 511 $("html,body").stop(true, true); 512}; 513