Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Chore: Simplify results table hooks #688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ classifiers = [
]
dependencies = [
"pytest>=7.0.0",
"pytest-metadata>=2.0.2",
"pytest-metadata>=3.0.0",
"Jinja2>=3.0.0",
]
dynamic = [
Expand Down
72 changes: 50 additions & 22 deletions src/pytest_html/basereport.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
import json
import math
import os
import re
import warnings
from pathlib import Path

import pytest
from pytest_metadata.plugin import metadata_key

from pytest_html import __version__
from pytest_html import extras
from pytest_html.table import Header
from pytest_html.table import Row
from pytest_html.util import cleanup_unserializable


Expand Down Expand Up @@ -60,8 +60,8 @@ def _generate_report(self, self_contained=False):

self._write_report(rendered_report)

def _generate_environment(self):
metadata = self._config._metadata
def _generate_environment(self, metadata_key):
metadata = self._config.stash[metadata_key]
for key in metadata.keys():
value = metadata[key]
if self._is_redactable_environment_variable(key):
Expand Down Expand Up @@ -145,16 +145,12 @@ def _write_report(self, rendered_report):

@pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session):
config = session.config
if hasattr(config, "_metadata") and config._metadata:
self._report.set_data("environment", self._generate_environment())
self._report.set_data("environment", self._generate_environment(metadata_key))

session.config.hook.pytest_html_report_title(report=self._report)

header_cells = Header()
session.config.hook.pytest_html_results_table_header(cells=header_cells)
self._report.set_data("resultsTableHeader", header_cells.html)
self._report.set_data("headerPops", header_cells.get_pops())
headers = self._report.data["resultsTableHeader"]
session.config.hook.pytest_html_results_table_header(cells=headers)

self._report.set_data("runningState", "Started")
self._generate_report()
Expand All @@ -173,7 +169,8 @@ def pytest_sessionfinish(self, session):
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_sep(
"-", f"Generated html report: file://{self._report_path.resolve()}"
"-",
f"Generated html report: file://{self._report_path.resolve().as_posix()}",
)

@pytest.hookimpl(trylast=True)
Expand All @@ -189,34 +186,60 @@ def pytest_runtest_logreport(self, report):
)

data = {
"duration": report.duration,
"result": _process_outcome(report),
"duration": _format_duration(report.duration),
}

total_duration = self._report.data["totalDuration"]
total_duration["total"] += report.duration
total_duration["formatted"] = _format_duration(total_duration["total"])

test_id = report.nodeid
if report.when != "call":
test_id += f"::{report.when}"
data["testId"] = test_id

row_cells = Row()
self._config.hook.pytest_html_results_table_row(report=report, cells=row_cells)
if row_cells.html is None:
data["extras"] = self._process_extras(report, test_id)
links = [
extra
for extra in data["extras"]
if extra["format_type"] in ["json", "text", "url"]
]
cells = [
f'<td class="col-result">{data["result"]}</td>',
f'<td class="col-name">{data["testId"]}</td>',
f'<td class="col-duration">{data["duration"]}</td>',
f'<td class="col-links">{_process_links(links)}</td>',
]

self._config.hook.pytest_html_results_table_row(report=report, cells=cells)
if not cells:
return
data["resultsTableRow"] = row_cells.html
for sortable, value in row_cells.sortables.items():
data[sortable] = value

data["resultsTableRow"] = cells

processed_logs = _process_logs(report)
self._config.hook.pytest_html_results_table_html(
report=report, data=processed_logs
)

data["result"] = _process_outcome(report)
data["extras"] = self._process_extras(report, test_id)

if self._report.add_test(data, report, processed_logs):
self._generate_report()


def _format_duration(duration):
if duration < 1:
return "{} ms".format(round(duration * 1000))

hours = math.floor(duration / 3600)
remaining_seconds = duration % 3600
minutes = math.floor(remaining_seconds / 60)
remaining_seconds = remaining_seconds % 60
seconds = round(remaining_seconds)

return f"{hours:02d}:{minutes:02d}:{seconds:02d}"


def _is_error(report):
return report.when in ["setup", "teardown"] and report.outcome == "failed"

Expand Down Expand Up @@ -249,3 +272,8 @@ def _process_outcome(report):
return "XFailed"

return report.outcome.capitalize()


def _process_links(links):
a_tag = '<a target="_blank" href="{content}" class="col-links__extra {format_type}">{name}</a>'
return "".join([a_tag.format_map(link) for link in links])
14 changes: 13 additions & 1 deletion src/pytest_html/report_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,26 @@
class ReportData:
def __init__(self, config):
self._config = config

default_headers = [
'<th class="sortable" data-column-type="result">Result</th>',
'<th class="sortable" data-column-type="testId">Test</th>',
'<th class="sortable" data-column-type="duration">Duration</th>',
"<th>Links</th>",
]

self._data = {
"title": "",
"collectedItems": 0,
"totalDuration": {
"total": 0,
"formatted": "",
},
"runningState": "not_started",
"environment": {},
"tests": defaultdict(list),
"resultsTableHeader": {},
"additionalSummary": defaultdict(list),
"resultsTableHeader": default_headers,
}

collapsed = config.getini("render_collapsed")
Expand Down
11 changes: 0 additions & 11 deletions src/pytest_html/resources/index.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,9 @@
<td></td>
</tr>
</template>
<template id="template_a">
<a target="_blank"></a>
</template>
<template id="template_results-table__tbody">
<tbody class="results-table-row">
<tr class="collapsible">
<td class="col-result"></td>
<td class="col-name"></td>
<td class="col-duration"></td>
<td class="col-links"></td>
</tr>
<tr class="extras-row">
<td class="extra" colspan="4">
Expand All @@ -62,10 +55,6 @@
<template id="template_results-table__head">
<thead id="results-table-head">
<tr>
<th class="sortable" data-column-type="result">Result</th>
<th class="sortable" data-column-type="testId">Test</th>
<th class="sortable" data-column-type="duration">Duration</th>
<th>Links</th>
</tr>
</thead>
</template>
Expand Down
4 changes: 2 additions & 2 deletions src/pytest_html/resources/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/pytest_html/scripts/datamanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class DataManager {
get isFinished() {
return this.data.runningState === 'Finished'
}
get formattedDuration() {
return this.data.totalDuration.formatted
}
}

module.exports = {
Expand Down
61 changes: 12 additions & 49 deletions src/pytest_html/scripts/dom.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const storageModule = require('./storage.js')
const { formatDuration, transformTableObj } = require('./utils.js')
const mediaViewer = require('./mediaviewer.js')
const templateEnvRow = document.querySelector('#template_environment_row')
const templateCollGroup = document.querySelector('#template_table-colgroup')
const templateResult = document.querySelector('#template_results-table__tbody')
const aTag = document.querySelector('#template_a')
const listHeader = document.querySelector('#template_results-table__head')
const listHeaderEmpty = document.querySelector('#template_results-table__head--empty')

Expand All @@ -28,12 +26,6 @@ const findAll = (selector, elem) => {
return [...elem.querySelectorAll(selector)]
}

const insertAdditionalHTML = (html, element, selector, position = 'beforebegin') => {
Object.keys(html).map((key) => {
element.querySelectorAll(selector).item(key).insertAdjacentHTML(position, html[key])
})
}

const dom = {
getStaticRow: (key, value) => {
const envRow = templateEnvRow.content.cloneNode(true)
Expand All @@ -53,29 +45,14 @@ const dom = {
const sortAttr = storageModule.getSort()
const sortAsc = JSON.parse(storageModule.getSortDirection())

const regex = /data-column-type="(\w+)/
const cols = Object.values(resultsTableHeader).reduce((result, value) => {
if (value.includes('sortable')) {
const matches = regex.exec(value)
if (matches) {
result.push(matches[1])
}
}
return result
}, [])
const sortables = ['result', 'testId', 'duration', ...cols]

// Add custom html from the pytest_html_results_table_header hook
const headers = transformTableObj(resultsTableHeader)
insertAdditionalHTML(headers.inserts, header, 'th')
insertAdditionalHTML(headers.appends, header, 'tr', 'beforeend')

sortables.forEach((sortCol) => {
if (sortCol === sortAttr) {
header.querySelector(`[data-column-type="${sortCol}"]`).classList.add(sortAsc ? 'desc' : 'asc')
}
resultsTableHeader.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
header.querySelector('#results-table-head > tr').appendChild(t.content)
})

header.querySelector(`.sortable[data-column-type="${sortAttr}"]`).classList.add(sortAsc ? 'desc' : 'asc')

return header
},
getListHeaderEmpty: () => listHeaderEmpty.content.cloneNode(true),
Expand All @@ -86,12 +63,13 @@ const dom = {
resultBody.querySelector('tbody').classList.add(resultLower)
resultBody.querySelector('tbody').id = testId
resultBody.querySelector('.collapsible').dataset.id = id
resultBody.querySelector('.col-result').innerText = result
resultBody.querySelector('.col-result').classList.add(`${collapsed ? 'expander' : 'collapser'}`)
resultBody.querySelector('.col-name').innerText = testId

const formattedDuration = duration < 1 ? formatDuration(duration).ms : formatDuration(duration).formatted
resultBody.querySelector('.col-duration').innerText = formattedDuration
resultsTableRow.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
resultBody.querySelector('.collapsible').appendChild(t.content)
})
resultBody.querySelector('.collapsible > td')?.classList.add(`${collapsed ? 'expander' : 'collapser'}`)

if (log) {
// Wrap lines starting with "E" with span.error to color those lines red
Expand All @@ -107,16 +85,6 @@ const dom = {

const media = []
extras?.forEach(({ name, format_type, content }) => {
if (['json', 'text', 'url'].includes(format_type)) {
const extraLink = aTag.content.cloneNode(true)
const extraLinkItem = extraLink.querySelector('a')

extraLinkItem.href = content
extraLinkItem.className = `col-links__extra ${format_type}`
extraLinkItem.innerText = name
resultBody.querySelector('.col-links').appendChild(extraLinkItem)
}

if (['image', 'video'].includes(format_type)) {
media.push({ path: content, name, format_type })
}
Expand All @@ -127,11 +95,6 @@ const dom = {
})
mediaViewer.setUp(resultBody, media)

// Add custom html from the pytest_html_results_table_row hook
const rows = transformTableObj(resultsTableRow)
resultsTableRow && insertAdditionalHTML(rows.inserts, resultBody, 'td')
resultsTableRow && insertAdditionalHTML(rows.appends, resultBody, 'tr', 'beforeend')

// Add custom html from the pytest_html_results_table_html hook
tableHtml?.forEach((item) => {
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
Expand Down
Loading