Skip to content

Commit

Permalink
feat: more html keyboard shortcuts
Browse files Browse the repository at this point in the history
On each page the shortcuts '[' and ']' will take you to the previous and
next files respectively. On the index page they take you to the final
and first files respectively.

Pressing 'u' while on a file will now take you back to index.html, like
on Gerrit.

'?' opens and closes the help panel.

Test cases:

    $ pytest --cov-report html --cov=. tests.py

in a directory with just tests.py, then with one, two or three .py
files.

Tested on Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:99.0) Gecko/20100101 Firefox/99.0
  • Loading branch information
jftsang authored and nedbat committed May 19, 2022
1 parent 114b762 commit 5b2e525
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 13 deletions.
61 changes: 55 additions & 6 deletions coverage/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def __init__(self, cov):
self.datagen = HtmlDataGeneration(self.coverage)
self.totals = Numbers(precision=self.config.precision)
self.directory_was_empty = False
self.first_fr = None
self.final_fr = None

self.template_globals = {
# Functions available in the templates.
Expand Down Expand Up @@ -204,9 +206,29 @@ def report(self, morfs):
self.incr.read()
self.incr.check_global_data(self.config, self.pyfile_html_source)

# Process all the files.
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
self.html_file(fr, analysis)
# Process all the files. For each page we need to supply a link
# to the next page. Therefore in each iteration of the loop we
# work on the fr and analysis from the previous iteration. We
# also need a link to the preceding page (i.e. 2 before the
# current iteration).
analysis_to_report = get_analysis_to_report(self.coverage, morfs)
pluprev_fr, prev_fr = None, None
prev_analysis = None

for fr, analysis in analysis_to_report:
if prev_fr is not None:
self.html_file(prev_fr, prev_analysis, pluprev_fr, fr)
else:
# This is the first file processed
self.first_fr = fr
pluprev_fr, prev_fr, prev_analysis = prev_fr, fr, analysis

# One more iteration for the final file. (Or not, if there are
# no files at all.)
if prev_fr is not None:
self.html_file(prev_fr, prev_analysis, pluprev_fr, None)
# This is the last file processed
self.final_fr = prev_fr

if not self.all_files_nums:
raise NoDataError("No data to report.")
Expand Down Expand Up @@ -236,10 +258,21 @@ def make_local_static_report_files(self):
if self.extra_css:
shutil.copyfile(self.config.extra_css, os.path.join(self.directory, self.extra_css))

def html_file(self, fr, analysis):
"""Generate an HTML file for one source file."""
def html_file(self, fr, analysis, prev_fr=None, next_fr=None):
"""Generate an HTML file for one source file, with links to the
previous and the next file, or to the index."""
rootname = flat_rootname(fr.relative_filename())
html_filename = rootname + ".html"
if prev_fr is not None:
prev_html = flat_rootname(prev_fr.relative_filename()) + ".html"
else:
prev_html = "index.html"

if next_fr is not None:
next_html = flat_rootname(next_fr.relative_filename()) + ".html"
else:
next_html = "index.html"

ensure_dir(self.directory)
if not os.listdir(self.directory):
self.directory_was_empty = True
Expand Down Expand Up @@ -316,7 +349,11 @@ def html_file(self, fr, analysis):
css_classes.append(self.template_globals['category'][ldata.category])
ldata.css_class = ' '.join(css_classes) or "pln"

html = self.source_tmpl.render(file_data.__dict__)
html = self.source_tmpl.render({
**file_data.__dict__,
'prev_html': prev_html,
'next_html': next_html,
})
write_html(html_path, html)

# Save this file's information for the index file.
Expand All @@ -340,11 +377,23 @@ def index_file(self):
n = self.skipped_empty_count
skipped_empty_msg = f"{n} empty file{plural(n)} skipped."

if self.first_fr is not None:
first_html = flat_rootname(self.first_fr.relative_filename()) + ".html"
else:
first_html = "index.html"

if self.final_fr is not None:
final_html = flat_rootname(self.final_fr.relative_filename()) + ".html"
else:
final_html = "index.html"

html = index_tmpl.render({
'files': self.file_summaries,
'totals': self.totals,
'skipped_covered_msg': skipped_covered_msg,
'skipped_empty_msg': skipped_empty_msg,
'first_html': first_html,
'final_html': final_html,
})

index_file = os.path.join(self.directory, "index.html")
Expand Down
41 changes: 35 additions & 6 deletions coverage/htmlfiles/coverage_html.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ function checkVisible(element) {
return !(rect.bottom < viewTop || rect.top >= viewBottom);
}

function on_click(sel, fn) {
const elt = document.querySelector(sel);
if (elt) {
elt.addEventListener("click", fn);
}
}

// Helpers for table sorting
function getCellValue(row, column = 0) {
const cell = row.cells[column]
Expand Down Expand Up @@ -193,6 +200,11 @@ coverage.index_ready = function () {
direction: th.getAttribute("aria-sort"),
}));
});

on_click(".button_prev_file", coverage.to_prev_file);
on_click(".button_next_file", coverage.to_next_file);

on_click(".button_show_hide_help", coverage.show_hide_help);
};

// -- pyfile stuff --
Expand All @@ -209,12 +221,6 @@ coverage.pyfile_ready = function () {
coverage.set_sel(0);
}

const on_click = function(sel, fn) {
const elt = document.querySelector(sel);
if (elt) {
elt.addEventListener("click", fn);
}
}
on_click(".button_toggle_run", coverage.toggle_lines);
on_click(".button_toggle_mis", coverage.toggle_lines);
on_click(".button_toggle_exc", coverage.toggle_lines);
Expand All @@ -225,6 +231,12 @@ coverage.pyfile_ready = function () {
on_click(".button_top_of_page", coverage.to_top);
on_click(".button_first_chunk", coverage.to_first_chunk);

on_click(".button_prev_file", coverage.to_prev_file);
on_click(".button_next_file", coverage.to_next_file);
on_click(".button_to_index", coverage.to_index);

on_click(".button_show_hide_help", coverage.show_hide_help);

coverage.filters = undefined;
try {
coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE);
Expand Down Expand Up @@ -299,6 +311,23 @@ coverage.to_first_chunk = function () {
coverage.to_next_chunk();
};

coverage.to_prev_file = function () {
window.location = document.getElementById("prevFileLink").href;
}

coverage.to_next_file = function () {
window.location = document.getElementById("nextFileLink").href;
}

coverage.to_index = function () {
location.href = document.getElementById("indexLink").href;
}

coverage.show_hide_help = function () {
const helpCheck = document.getElementById("help_panel_state")
helpCheck.checked = !helpCheck.checked;
}

// Return a string indicating what kind of chunk this line belongs to,
// or null if not a chunk.
coverage.chunk_indicator = function (line_elt) {
Expand Down
17 changes: 17 additions & 0 deletions coverage/htmlfiles/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ <h1>{{ title|escape }}:
{% endif %}
<kbd>c</kbd> &nbsp; change column sorting
</p>
<p class="keyhelp">
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p class="keyhelp">
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -115,6 +123,15 @@ <h1>{{ title|escape }}:
created at {{ time_stamp }}
</p>
</div>
<div style="display: none;">
<p>
<a id="prevFileLink" class="nav" href="{{ final_html }}">first file</a>
<a id="nextFileLink" class="nav" href="{{ first_html }}">final file</a>
</p>
<button type="button" class="button_prev_file" data-shortcut="[">Previous file</button>
<button type="button" class="button_next_file" data-shortcut="]">Next file</button>
<button type="button" class="button_show_hide_help" data-shortcut="?">Show/hide keyboard shortcuts</button>
</div>
</footer>

</body>
Expand Down
22 changes: 21 additions & 1 deletion coverage/htmlfiles/pyfile.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ <h1>
<p class="keyhelp">
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p class="keyhelp">
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p class="keyhelp">
<kbd>u</kbd> &nbsp; back to the index
</p>
<p class="keyhelp">
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</div>
Expand All @@ -71,6 +82,10 @@ <h2>
<button type="button" class="button_prev_chunk" data-shortcut="k">Previous highlighted chunk</button>
<button type="button" class="button_top_of_page" data-shortcut="0">Goto top of page</button>
<button type="button" class="button_first_chunk" data-shortcut="1">Goto first highlighted chunk</button>
<button type="button" class="button_prev_file" data-shortcut="[">Previous file</button>
<button type="button" class="button_next_file" data-shortcut="]">Next file</button>
<button type="button" class="button_to_index" data-shortcut="u">Back to the index</button>
<button type="button" class="button_show_hide_help" data-shortcut="?">Show/hide keyboard shortcuts</button>
</div>
</div>
</header>
Expand Down Expand Up @@ -110,7 +125,12 @@ <h2>
<footer>
<div class="content">
<p>
<a class="nav" href="index.html">&#xab; index</a> &nbsp; &nbsp; <a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>,
<a id="prevFileLink" class="nav" href="{{ prev_html }}">&#xab; prev file</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="{{ next_html }}">&#xbb; next file</a>
</p>
<p>
<a class="nav" href="{{__url__}}">coverage.py v{{__version__}}</a>,
created at {{ time_stamp }}
</p>
</div>
Expand Down

0 comments on commit 5b2e525

Please sign in to comment.