testing-html-merge #15

Merged
dan merged 8 commits from testing-html-merge into dev 2023-12-09 15:28:50 +01:00
11 changed files with 2762 additions and 19 deletions

View file

@ -149,6 +149,7 @@ playwright codegen demo.playwright.dev/todomvc # visit given url in codegen mod
```bash ```bash
pytest # test pytest-abra pytest # test pytest-abra
pytest -m "not slow" # test pytest-abra without slow tests
pytest --collect-only # debug test pytest-abra pytest --collect-only # debug test pytest-abra
docker compose run --rm app pytest # run pytest-abra docker compose run --rm app pytest # run pytest-abra
``` ```

View file

@ -52,4 +52,7 @@ target-version = "py311"
[tool.pytest.ini_options] [tool.pytest.ini_options]
python_functions = "setup_* test_* cleanup_*" python_functions = "setup_* test_* cleanup_*"
norecursedirs = ".* previous-work recipes" norecursedirs = ".* previous-work recipes"
testpaths = "tests" testpaths = "tests"
markers = [
"slow: marks tests as slow",
]

View file

@ -7,7 +7,7 @@ from loguru import logger
from pytest_abra.dir_manager import DirManager from pytest_abra.dir_manager import DirManager
from pytest_abra.env_manager import EnvFile, EnvManager from pytest_abra.env_manager import EnvFile, EnvManager
from pytest_abra.html_helper import merge_html_files from pytest_abra.html_helper import merge_html_reports
from pytest_abra.runner import Runner from pytest_abra.runner import Runner
from pytest_abra.utils import rmtree from pytest_abra.utils import rmtree
@ -58,10 +58,10 @@ class Coordinator:
def combine_html(self) -> None: def combine_html(self) -> None:
"""combines all generated pytest html reports into one""" """combines all generated pytest html reports into one"""
in_path = str(self.DIR.RECORDS / "html") in_dir_path = str(self.DIR.RECORDS / "html")
out_path = str(self.DIR.RECORDS / "full-report.html") out_file_path = str(self.DIR.RECORDS / "full-report.html")
title = "combined.html" title = "combined.html"
merge_html_files(in_path, out_path, title) merge_html_reports(in_dir_path, out_file_path, title)
def collect_traces(self): def collect_traces(self):
"""moves all traces into SESSION/RECORDS dir """moves all traces into SESSION/RECORDS dir

View file

@ -5,19 +5,36 @@ import json
import os import os
import pathlib import pathlib
import re import re
import shutil
from bs4 import BeautifulSoup from bs4 import BeautifulSoup # type: ignore
from packaging import version from packaging import version
CHECKBOX_REGEX = r"^(?P<num>0|[1-9]\d*) (?P<txt1>.*)" CHECKBOX_REGEX = r"^(?P<num>0|[1-9]\d*) (?P<txt1>.*)"
def merge_html_files(in_path: str, out_path: str, title: str): def custom_copy_assets(assets_dir_path: str, out_file_path: str):
paths = get_html_files(in_path, out_path) """custom function added for pytest_abra
if not paths:
raise RuntimeError(f"Unable to find html files in {in_path}")
assets_dir_path = get_assets_path(in_path) copies every asset to asset folder. Exclude style.css as this is already handled by pytest_html_merger"""
assets_source_dir = pathlib.Path(assets_dir_path)
assets_source_files = [p for p in assets_source_dir.glob("*") if p.is_file() and p.name != "style.css"]
out_dir_path = pathlib.Path(out_file_path).parent
assets_target_dir = out_dir_path / "assets"
assets_target_dir.mkdir(exist_ok=True)
for asset in assets_source_files:
shutil.copy(asset, assets_target_dir / asset.name)
def merge_html_reports(in_dir_path: str, out_file_path: str, report_title: str):
paths = get_html_files(in_dir_path, out_file_path)
if not paths:
raise RuntimeError(f"Unable to find html files in {in_dir_path}")
assets_dir_path = get_assets_path(in_dir_path)
custom_copy_assets(assets_dir_path, out_file_path)
first_file = BeautifulSoup("".join(open(paths[0])), features="html.parser") first_file = BeautifulSoup("".join(open(paths[0])), features="html.parser")
paths.pop(0) paths.pop(0)
@ -30,7 +47,7 @@ def merge_html_files(in_path: str, out_path: str, title: str):
if assets_dir_path is None: if assets_dir_path is None:
print( print(
f"Will assume css is embedded in the reports. If this is not the case, " f"Will assume css is embedded in the reports. If this is not the case, "
f"Please make sure that you have 'assets' directory inside {in_path} " f"Please make sure that you have 'assets' directory inside {in_dir_path} "
f"which contains css files generated by pytest-html." f"which contains css files generated by pytest-html."
) )
else: else:
@ -42,7 +59,7 @@ def merge_html_files(in_path: str, out_path: str, title: str):
head.style.append(content) head.style.append(content)
h = first_file.find("h1") h = first_file.find("h1")
h.string = title or os.path.basename(out_path) h.string = report_title or os.path.basename(out_file_path)
ps = first_file.find_all("p") ps = first_file.find_all("p")
pytest_version = ps[0].text.split(" ")[-1] pytest_version = ps[0].text.split(" ")[-1]
@ -106,7 +123,7 @@ def merge_html_files(in_path: str, out_path: str, title: str):
for cb_type in cb_types: for cb_type in cb_types:
set_checkbox_value(first_file, cb_type, cb_types[cb_type]) set_checkbox_value(first_file, cb_type, cb_types[cb_type])
with open(out_path, "w") as f: with open(out_file_path, "w") as f:
f.write(str(first_file)) f.write(str(first_file))

View file

@ -57,7 +57,7 @@ class Runner:
"""runs the main test script and if available and sub test scripts if their running condition is met""" """runs the main test script and if available and sub test scripts if their running condition is met"""
# check if required dependencies have passed # check if required dependencies have passed
if not self._dependencies_passed(): if not self._dependencies_passed():
logger.warning(f"skipping run_tests() of {self.env_type}, because some dependencies have not passed") logger.warning(f"skipping run_tests() of {self.env_type} (one or more dependencies have not passed)")
return return
for test in test_list: for test in test_list:
@ -79,16 +79,16 @@ class Runner:
# check if test aleady passed # check if test aleady passed
if self._is_test_passed(identifier_string, remove_existing=True): if self._is_test_passed(identifier_string, remove_existing=True):
if test.prevent_skip: if test.prevent_skip:
logger.info(f"continuing , test {identifier_string} has passed but prevent_skip=True") logger.info(f"continuing {identifier_string} (passed before but prevent_skip=True)")
else: else:
logger.info(f"skipping {identifier_string}, test has passed") logger.info(f"skipping {identifier_string} (test has passed)")
return return
if test.condition: if test.condition:
condition_result = self._run_condition(test.condition) condition_result = self._run_condition(test.condition)
if not condition_result: if not condition_result:
# test condition is defined but not met # test condition is defined but not met
logger.info(f"skipping {identifier_string}, test condition is not met") logger.info(f"skipping {identifier_string} (test condition is not met)")
return return
# test condition is undefined or not met # test condition is undefined or not met
@ -126,7 +126,7 @@ class Runner:
return already_passed return already_passed
def _call_pytest(self, full_test_path: Path) -> int: def _call_pytest(self, full_test_path: Path) -> int:
"""runs pytest programmatically on a specific file """runs pytest programmatically with a specific file
all tests in the file [full_test_path] will be run along with command line arguments""" all tests in the file [full_test_path] will be run along with command line arguments"""

View file

@ -0,0 +1,33 @@
name enabled status
-------------------------------------------------------- --------- ----------
Custom Invalidation Flow True successful
System - SCIM Provider - Mappings True successful
System - OAuth2 Provider - Scopes True successful
System - SAML Provider - Mappings True successful
System - LDAP Source - Mappings True successful
Migration - Remove old prompt fields True successful
Default - Events Transport & Rules True successful
Default - Source pre-authentication flow True successful
Default - TOTP MFA setup flow True successful
Default - WebAuthn MFA setup flow True successful
Default - Provider authorization flow (explicit consent) True failed
Default - Source authentication flow True successful
Default - Provider authorization flow (implicit consent) True successful
Default - Static MFA setup flow True successful
matrix True successful
Custom System Tenant True successful
Nextcloud True successful
Wordpress True successful
Custom Authentication Flow True successful
wekan True successful
Default - Invalidation flow True successful
Default - Tenant True successful
Flow Translations True successful
Default - User settings flow False successful
Default - Source enrollment flow False successful
Invitation Enrollment Flow True successful
vikunja True successful
Default - Password change flow False successful
Default - Authentication flow False successful
Recovery with email verification True successful
System - Proxy Provider - Scopes True successful

View file

@ -0,0 +1,319 @@
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
/* do not increase min-width as some may use split screens */
min-width: 800px;
color: #999;
}
h1 {
font-size: 24px;
color: black;
}
h2 {
font-size: 16px;
color: black;
}
p {
color: black;
}
a {
color: #999;
}
table {
border-collapse: collapse;
}
/******************************
* SUMMARY INFORMATION
******************************/
#environment td {
padding: 5px;
border: 1px solid #e6e6e6;
vertical-align: top;
}
#environment tr:nth-child(odd) {
background-color: #f6f6f6;
}
#environment ul {
margin: 0;
padding: 0 20px;
}
/******************************
* TEST RESULT COLORS
******************************/
span.passed,
.passed .col-result {
color: green;
}
span.skipped,
span.xfailed,
span.rerun,
.skipped .col-result,
.xfailed .col-result,
.rerun .col-result {
color: orange;
}
span.error,
span.failed,
span.xpassed,
.error .col-result,
.failed .col-result,
.xpassed .col-result {
color: red;
}
.col-links__extra {
margin-right: 3px;
}
/******************************
* RESULTS TABLE
*
* 1. Table Layout
* 2. Extra
* 3. Sorting items
*
******************************/
/*------------------
* 1. Table Layout
*------------------*/
#results-table {
border: 1px solid #e6e6e6;
color: #999;
font-size: 12px;
width: 100%;
}
#results-table th,
#results-table td {
padding: 5px;
border: 1px solid #e6e6e6;
text-align: left;
}
#results-table th {
font-weight: bold;
}
/*------------------
* 2. Extra
*------------------*/
.logwrapper {
max-height: 230px;
overflow-y: scroll;
background-color: #e6e6e6;
}
.logwrapper.expanded {
max-height: none;
}
.logwrapper.expanded .logexpander:after {
content: "collapse [-]";
}
.logwrapper .logexpander {
z-index: 1;
position: sticky;
top: 10px;
width: max-content;
border: 1px solid;
border-radius: 3px;
padding: 5px 7px;
margin: 10px 0 10px calc(100% - 80px);
cursor: pointer;
background-color: #e6e6e6;
}
.logwrapper .logexpander:after {
content: "expand [+]";
}
.logwrapper .logexpander:hover {
color: #000;
border-color: #000;
}
.logwrapper .log {
min-height: 40px;
position: relative;
top: -50px;
height: calc(100% + 50px);
border: 1px solid #e6e6e6;
color: black;
display: block;
font-family: "Courier New", Courier, monospace;
padding: 5px;
padding-right: 80px;
white-space: pre-wrap;
}
div.media {
border: 1px solid #e6e6e6;
float: right;
height: 240px;
margin: 0 5px;
overflow: hidden;
width: 320px;
}
.media-container {
display: grid;
grid-template-columns: 25px auto 25px;
align-items: center;
flex: 1 1;
overflow: hidden;
height: 200px;
}
.media-container--fullscreen {
grid-template-columns: 0px auto 0px;
}
.media-container__nav--right,
.media-container__nav--left {
text-align: center;
cursor: pointer;
}
.media-container__viewport {
cursor: pointer;
text-align: center;
height: inherit;
}
.media-container__viewport img,
.media-container__viewport video {
object-fit: cover;
width: 100%;
max-height: 100%;
}
.media__name,
.media__counter {
display: flex;
flex-direction: row;
justify-content: space-around;
flex: 0 0 25px;
align-items: center;
}
.collapsible td:not(.col-links) {
cursor: pointer;
}
.collapsible td:not(.col-links):hover::after {
color: #bbb;
font-style: italic;
cursor: pointer;
}
.col-result {
width: 130px;
}
.col-result:hover::after {
content: " (hide details)";
}
.col-result.collapsed:hover::after {
content: " (show details)";
}
#environment-header h2:hover::after {
content: " (hide details)";
color: #bbb;
font-style: italic;
cursor: pointer;
font-size: 12px;
}
#environment-header.collapsed h2:hover::after {
content: " (show details)";
color: #bbb;
font-style: italic;
cursor: pointer;
font-size: 12px;
}
/*------------------
* 3. Sorting items
*------------------*/
.sortable {
cursor: pointer;
}
.sortable.desc:after {
content: " ";
position: relative;
left: 5px;
bottom: -12.5px;
border: 10px solid #4caf50;
border-bottom: 0;
border-left-color: transparent;
border-right-color: transparent;
}
.sortable.asc:after {
content: " ";
position: relative;
left: 5px;
bottom: 12.5px;
border: 10px solid #4caf50;
border-top: 0;
border-left-color: transparent;
border-right-color: transparent;
}
.hidden, .summary__reload__button.hidden {
display: none;
}
.summary__data {
flex: 0 0 550px;
}
.summary__reload {
flex: 1 1;
display: flex;
justify-content: center;
}
.summary__reload__button {
flex: 0 0 300px;
display: flex;
color: white;
font-weight: bold;
background-color: #4caf50;
text-align: center;
justify-content: center;
align-items: center;
border-radius: 3px;
cursor: pointer;
}
.summary__reload__button:hover {
background-color: #46a049;
}
.summary__spacer {
flex: 0 0 550px;
}
.controls {
display: flex;
justify-content: space-between;
}
.filters,
.collapse {
display: flex;
align-items: center;
}
.filters button,
.collapse button {
color: #999;
border: none;
background: none;
cursor: pointer;
text-decoration: underline;
}
.filters button:hover,
.collapse button:hover {
color: #ccc;
}
.filter__label {
margin-right: 10px;
}

View file

@ -0,0 +1,770 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title id="head-title">setup_wordpress.html</title>
<link href="assets\style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<h1 id="title">setup_wordpress.html</h1>
<p>Report generated on 08-Dec-2023 at 14:55:57 by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a>
v4.1.1</p>
<div id="environment-header">
<h2>Environment</h2>
</div>
<table id="environment"></table>
<!-- TEMPLATES -->
<template id="template_environment_row">
<tr>
<td></td>
<td></td>
</tr>
</template>
<template id="template_results-table__body--empty">
<tbody class="results-table-row">
<tr id="not-found-message">
<td colspan="4">No results found. Check the filters.</th>
</tr>
</template>
<template id="template_results-table__tbody">
<tbody class="results-table-row">
<tr class="collapsible">
</tr>
<tr class="extras-row">
<td class="extra" colspan="4">
<div class="extraHTML"></div>
<div class="media">
<div class="media-container">
<div class="media-container__nav--left"><</div>
<div class="media-container__viewport">
<img src="" />
<video controls>
<source src="" type="video/mp4">
</video>
</div>
<div class="media-container__nav--right">></div>
</div>
<div class="media__name"></div>
<div class="media__counter"></div>
</div>
<div class="logwrapper">
<div class="logexpander"></div>
<div class="log"></div>
</div>
</td>
</tr>
</tbody>
</template>
<!-- END TEMPLATES -->
<div class="summary">
<div class="summary__data">
<h2>Summary</h2>
<div class="additional-summary prefix">
</div>
<p class="run-count">2 tests took 00:00:11.</p>
<p class="filter">(Un)check the boxes to filter the results.</p>
<div class="summary__reload">
<div class="summary__reload__button hidden" onclick="location.reload()">
<div>There are still tests running. <br />Reload this page to get the latest results!</div>
</div>
</div>
<div class="summary__spacer"></div>
<div class="controls">
<div class="filters">
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="failed" disabled/>
<span class="failed">0 Failed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="passed" />
<span class="passed">2 Passed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="skipped" disabled/>
<span class="skipped">0 Skipped,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xfailed" disabled/>
<span class="xfailed">0 Expected failures,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xpassed" disabled/>
<span class="xpassed">0 Unexpected passes,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="error" disabled/>
<span class="error">0 Errors,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="rerun" disabled/>
<span class="rerun">0 Reruns</span>
</div>
<div class="collapse">
<button id="show_all_details">Show all details</button>&nbsp;/&nbsp;<button id="hide_all_details">Hide all details</button>
</div>
</div>
</div>
<div class="additional-summary summary">
</div>
<div class="additional-summary postfix">
</div>
</div>
<table id="results-table">
<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>
</table>
</body>
<footer>
<div id="data-container" data-jsonblob="{&#34;environment&#34;: {&#34;Python&#34;: &#34;3.10.13&#34;, &#34;Platform&#34;: &#34;Windows-10-10.0.19045-SP0&#34;, &#34;Packages&#34;: {&#34;pytest&#34;: &#34;7.4.3&#34;, &#34;pluggy&#34;: &#34;1.3.0&#34;}, &#34;Plugins&#34;: {&#34;abra&#34;: &#34;0.2.0&#34;, &#34;base-url&#34;: &#34;2.0.0&#34;, &#34;html&#34;: &#34;4.1.1&#34;, &#34;metadata&#34;: &#34;3.0.0&#34;, &#34;playwright&#34;: &#34;0.4.3&#34;}}, &#34;tests&#34;: {&#34;recipes/wordpress/tests_wordpress/setup_wordpress.py::test_visit_from_domain[chromium]&#34;: [{&#34;extras&#34;: [], &#34;result&#34;: &#34;Passed&#34;, &#34;testId&#34;: &#34;recipes/wordpress/tests_wordpress/setup_wordpress.py::test_visit_from_domain[chromium]&#34;, &#34;duration&#34;: &#34;00:00:04&#34;, &#34;resultsTableRow&#34;: [&#34;&lt;td class=\&#34;col-result\&#34;&gt;Passed&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-testId\&#34;&gt;recipes/wordpress/tests_wordpress/setup_wordpress.py::test_visit_from_domain[chromium]&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-duration\&#34;&gt;00:00:04&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-links\&#34;&gt;&lt;/td&gt;&#34;], &#34;log&#34;: &#34;No log output captured.&#34;}], &#34;recipes/wordpress/tests_wordpress/setup_wordpress.py::setup_wordpress_admin_session[chromium]&#34;: [{&#34;extras&#34;: [], &#34;result&#34;: &#34;Passed&#34;, &#34;testId&#34;: &#34;recipes/wordpress/tests_wordpress/setup_wordpress.py::setup_wordpress_admin_session[chromium]&#34;, &#34;duration&#34;: &#34;00:00:07&#34;, &#34;resultsTableRow&#34;: [&#34;&lt;td class=\&#34;col-result\&#34;&gt;Passed&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-testId\&#34;&gt;recipes/wordpress/tests_wordpress/setup_wordpress.py::setup_wordpress_admin_session[chromium]&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-duration\&#34;&gt;00:00:07&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-links\&#34;&gt;&lt;/td&gt;&#34;], &#34;log&#34;: &#34;No log output captured.&#34;}]}, &#34;renderCollapsed&#34;: [&#34;passed&#34;], &#34;initialSort&#34;: &#34;result&#34;, &#34;title&#34;: &#34;setup_wordpress.html&#34;}"></div>
<script>
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const { getCollapsedCategory, setCollapsedIds } = require('./storage.js')
class DataManager {
setManager(data) {
const collapsedCategories = [...getCollapsedCategory(data.renderCollapsed)]
const collapsedIds = []
const tests = Object.values(data.tests).flat().map((test, index) => {
const collapsed = collapsedCategories.includes(test.result.toLowerCase())
const id = `test_${index}`
if (collapsed) {
collapsedIds.push(id)
}
return {
...test,
id,
collapsed,
}
})
const dataBlob = { ...data, tests }
this.data = { ...dataBlob }
this.renderData = { ...dataBlob }
setCollapsedIds(collapsedIds)
}
get allData() {
return { ...this.data }
}
resetRender() {
this.renderData = { ...this.data }
}
setRender(data) {
this.renderData.tests = [...data]
}
toggleCollapsedItem(id) {
this.renderData.tests = this.renderData.tests.map((test) =>
test.id === id ? { ...test, collapsed: !test.collapsed } : test,
)
}
set allCollapsed(collapsed) {
this.renderData = { ...this.renderData, tests: [...this.renderData.tests.map((test) => (
{ ...test, collapsed }
))] }
}
get testSubset() {
return [...this.renderData.tests]
}
get environment() {
return this.renderData.environment
}
get initialSort() {
return this.data.initialSort
}
}
module.exports = {
manager: new DataManager(),
}
},{"./storage.js":8}],2:[function(require,module,exports){
const mediaViewer = require('./mediaviewer.js')
const templateEnvRow = document.getElementById('template_environment_row')
const templateResult = document.getElementById('template_results-table__tbody')
function htmlToElements(html) {
const temp = document.createElement('template')
temp.innerHTML = html
return temp.content.childNodes
}
const find = (selector, elem) => {
if (!elem) {
elem = document
}
return elem.querySelector(selector)
}
const findAll = (selector, elem) => {
if (!elem) {
elem = document
}
return [...elem.querySelectorAll(selector)]
}
const dom = {
getStaticRow: (key, value) => {
const envRow = templateEnvRow.content.cloneNode(true)
const isObj = typeof value === 'object' && value !== null
const values = isObj ? Object.keys(value).map((k) => `${k}: ${value[k]}`) : null
const valuesElement = htmlToElements(
values ? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>` : `<div>${value}</div>`)[0]
const td = findAll('td', envRow)
td[0].textContent = key
td[1].appendChild(valuesElement)
return envRow
},
getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => {
const resultBody = templateResult.content.cloneNode(true)
resultBody.querySelector('tbody').classList.add(result.toLowerCase())
resultBody.querySelector('tbody').id = testId
resultBody.querySelector('.collapsible').dataset.id = id
resultsTableRow.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
resultBody.querySelector('.collapsible').appendChild(t.content)
})
if (log) {
// Wrap lines starting with "E" with span.error to color those lines red
const wrappedLog = log.replace(/^E.*$/gm, (match) => `<span class="error">${match}</span>`)
resultBody.querySelector('.log').innerHTML = wrappedLog
} else {
resultBody.querySelector('.log').remove()
}
if (collapsed) {
resultBody.querySelector('.collapsible > td')?.classList.add('collapsed')
resultBody.querySelector('.extras-row').classList.add('hidden')
} else {
resultBody.querySelector('.collapsible > td')?.classList.remove('collapsed')
}
const media = []
extras?.forEach(({ name, format_type, content }) => {
if (['image', 'video'].includes(format_type)) {
media.push({ path: content, name, format_type })
}
if (format_type === 'html') {
resultBody.querySelector('.extraHTML').insertAdjacentHTML('beforeend', `<div>${content}</div>`)
}
})
mediaViewer.setup(resultBody, media)
// Add custom html from the pytest_html_results_table_html hook
tableHtml?.forEach((item) => {
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
})
return resultBody
},
}
module.exports = {
dom,
htmlToElements,
find,
findAll,
}
},{"./mediaviewer.js":6}],3:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const storageModule = require('./storage.js')
const getFilteredSubSet = (filter) =>
manager.allData.tests.filter(({ result }) => filter.includes(result.toLowerCase()))
const doInitFilter = () => {
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
}
const doFilter = (type, show) => {
if (show) {
storageModule.showCategory(type)
} else {
storageModule.hideCategory(type)
}
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
const sortColumn = storageModule.getSort()
doSort(sortColumn, true)
}
module.exports = {
doFilter,
doInitFilter,
}
},{"./datamanager.js":1,"./sort.js":7,"./storage.js":8}],4:[function(require,module,exports){
const { redraw, bindEvents, renderStatic } = require('./main.js')
const { doInitFilter } = require('./filter.js')
const { doInitSort } = require('./sort.js')
const { manager } = require('./datamanager.js')
const data = JSON.parse(document.getElementById('data-container').dataset.jsonblob)
function init() {
manager.setManager(data)
doInitFilter()
doInitSort()
renderStatic()
redraw()
bindEvents()
}
init()
},{"./datamanager.js":1,"./filter.js":3,"./main.js":5,"./sort.js":7}],5:[function(require,module,exports){
const { dom, find, findAll } = require('./dom.js')
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const { doFilter } = require('./filter.js')
const {
getVisible,
getCollapsedIds,
setCollapsedIds,
getSort,
getSortDirection,
possibleFilters,
} = require('./storage.js')
const removeChildren = (node) => {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
const renderStatic = () => {
const renderEnvironmentTable = () => {
const environment = manager.environment
const rows = Object.keys(environment).map((key) => dom.getStaticRow(key, environment[key]))
const table = document.getElementById('environment')
removeChildren(table)
rows.forEach((row) => table.appendChild(row))
}
renderEnvironmentTable()
}
const addItemToggleListener = (elem) => {
elem.addEventListener('click', ({ target }) => {
const id = target.parentElement.dataset.id
manager.toggleCollapsedItem(id)
const collapsedIds = getCollapsedIds()
if (collapsedIds.includes(id)) {
const updated = collapsedIds.filter((item) => item !== id)
setCollapsedIds(updated)
} else {
collapsedIds.push(id)
setCollapsedIds(collapsedIds)
}
redraw()
})
}
const renderContent = (tests) => {
const sortAttr = getSort(manager.initialSort)
const sortAsc = JSON.parse(getSortDirection())
const rows = tests.map(dom.getResultTBody)
const table = document.getElementById('results-table')
const tableHeader = document.getElementById('results-table-head')
const newTable = document.createElement('table')
newTable.id = 'results-table'
// remove all sorting classes and set the relevant
findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc'))
tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc')
newTable.appendChild(tableHeader)
if (!rows.length) {
const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true)
newTable.appendChild(emptyTable)
} else {
rows.forEach((row) => {
if (!!row) {
findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener)
find('.logexpander', row).addEventListener('click',
(evt) => evt.target.parentNode.classList.toggle('expanded'),
)
newTable.appendChild(row)
}
})
}
table.replaceWith(newTable)
}
const renderDerived = () => {
const currentFilter = getVisible()
possibleFilters.forEach((result) => {
const input = document.querySelector(`input[data-test-result="${result}"]`)
input.checked = currentFilter.includes(result)
})
}
const bindEvents = () => {
const filterColumn = (evt) => {
const { target: element } = evt
const { testResult } = element.dataset
doFilter(testResult, element.checked)
const collapsedIds = getCollapsedIds()
const updated = manager.renderData.tests.map((test) => {
return {
...test,
collapsed: collapsedIds.includes(test.id),
}
})
manager.setRender(updated)
redraw()
}
const header = document.getElementById('environment-header')
header.addEventListener('click', () => {
const table = document.getElementById('environment')
table.classList.toggle('hidden')
header.classList.toggle('collapsed')
})
findAll('input[name="filter_checkbox"]').forEach((elem) => {
elem.addEventListener('click', filterColumn)
})
findAll('.sortable').forEach((elem) => {
elem.addEventListener('click', (evt) => {
const { target: element } = evt
const { columnType } = element.dataset
doSort(columnType)
redraw()
})
})
document.getElementById('show_all_details').addEventListener('click', () => {
manager.allCollapsed = false
setCollapsedIds([])
redraw()
})
document.getElementById('hide_all_details').addEventListener('click', () => {
manager.allCollapsed = true
const allIds = manager.renderData.tests.map((test) => test.id)
setCollapsedIds(allIds)
redraw()
})
}
const redraw = () => {
const { testSubset } = manager
renderContent(testSubset)
renderDerived()
}
module.exports = {
redraw,
bindEvents,
renderStatic,
}
},{"./datamanager.js":1,"./dom.js":2,"./filter.js":3,"./sort.js":7,"./storage.js":8}],6:[function(require,module,exports){
class MediaViewer {
constructor(assets) {
this.assets = assets
this.index = 0
}
nextActive() {
this.index = this.index === this.assets.length - 1 ? 0 : this.index + 1
return [this.activeFile, this.index]
}
prevActive() {
this.index = this.index === 0 ? this.assets.length - 1 : this.index -1
return [this.activeFile, this.index]
}
get currentIndex() {
return this.index
}
get activeFile() {
return this.assets[this.index]
}
}
const setup = (resultBody, assets) => {
if (!assets.length) {
resultBody.querySelector('.media').classList.add('hidden')
return
}
const mediaViewer = new MediaViewer(assets)
const container = resultBody.querySelector('.media-container')
const leftArrow = resultBody.querySelector('.media-container__nav--left')
const rightArrow = resultBody.querySelector('.media-container__nav--right')
const mediaName = resultBody.querySelector('.media__name')
const counter = resultBody.querySelector('.media__counter')
const imageEl = resultBody.querySelector('img')
const sourceEl = resultBody.querySelector('source')
const videoEl = resultBody.querySelector('video')
const setImg = (media, index) => {
if (media?.format_type === 'image') {
imageEl.src = media.path
imageEl.classList.remove('hidden')
videoEl.classList.add('hidden')
} else if (media?.format_type === 'video') {
sourceEl.src = media.path
videoEl.classList.remove('hidden')
imageEl.classList.add('hidden')
}
mediaName.innerText = media?.name
counter.innerText = `${index + 1} / ${assets.length}`
}
setImg(mediaViewer.activeFile, mediaViewer.currentIndex)
const moveLeft = () => {
const [media, index] = mediaViewer.prevActive()
setImg(media, index)
}
const doRight = () => {
const [media, index] = mediaViewer.nextActive()
setImg(media, index)
}
const openImg = () => {
window.open(mediaViewer.activeFile.path, '_blank')
}
if (assets.length === 1) {
container.classList.add('media-container--fullscreen')
} else {
leftArrow.addEventListener('click', moveLeft)
rightArrow.addEventListener('click', doRight)
}
imageEl.addEventListener('click', openImg)
}
module.exports = {
setup,
}
},{}],7:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const storageModule = require('./storage.js')
const genericSort = (list, key, ascending, customOrder) => {
let sorted
if (customOrder) {
sorted = list.sort((a, b) => {
const aValue = a.result.toLowerCase()
const bValue = b.result.toLowerCase()
const aIndex = customOrder.findIndex((item) => item.toLowerCase() === aValue)
const bIndex = customOrder.findIndex((item) => item.toLowerCase() === bValue)
// Compare the indices to determine the sort order
return aIndex - bIndex
})
} else {
sorted = list.sort((a, b) => a[key] === b[key] ? 0 : a[key] > b[key] ? 1 : -1)
}
if (ascending) {
sorted.reverse()
}
return sorted
}
const durationSort = (list, ascending) => {
const parseDuration = (duration) => {
if (duration.includes(':')) {
// If it's in the format "HH:mm:ss"
const [hours, minutes, seconds] = duration.split(':').map(Number)
return (hours * 3600 + minutes * 60 + seconds) * 1000
} else {
// If it's in the format "nnn ms"
return parseInt(duration)
}
}
const sorted = list.sort((a, b) => parseDuration(a['duration']) - parseDuration(b['duration']))
if (ascending) {
sorted.reverse()
}
return sorted
}
const doInitSort = () => {
const type = storageModule.getSort(manager.initialSort)
const ascending = storageModule.getSortDirection()
const list = manager.testSubset
const initialOrder = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed', 'Skipped', 'Passed']
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
if (type?.toLowerCase() === 'original') {
manager.setRender(list)
} else {
let sortedList
switch (type) {
case 'duration':
sortedList = durationSort(list, ascending)
break
case 'result':
sortedList = genericSort(list, type, ascending, initialOrder)
break
default:
sortedList = genericSort(list, type, ascending)
break
}
manager.setRender(sortedList)
}
}
const doSort = (type, skipDirection) => {
const newSortType = storageModule.getSort(manager.initialSort) !== type
const currentAsc = storageModule.getSortDirection()
let ascending
if (skipDirection) {
ascending = currentAsc
} else {
ascending = newSortType ? false : !currentAsc
}
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
const list = manager.testSubset
const sortedList = type === 'duration' ? durationSort(list, ascending) : genericSort(list, type, ascending)
manager.setRender(sortedList)
}
module.exports = {
doInitSort,
doSort,
}
},{"./datamanager.js":1,"./storage.js":8}],8:[function(require,module,exports){
const possibleFilters = [
'passed',
'skipped',
'failed',
'error',
'xfailed',
'xpassed',
'rerun',
]
const getVisible = () => {
const url = new URL(window.location.href)
const settings = new URLSearchParams(url.search).get('visible')
const lower = (item) => {
const lowerItem = item.toLowerCase()
if (possibleFilters.includes(lowerItem)) {
return lowerItem
}
return null
}
return settings === null ?
possibleFilters :
[...new Set(settings?.split(',').map(lower).filter((item) => item))]
}
const hideCategory = (categoryToHide) => {
const url = new URL(window.location.href)
const visibleParams = new URLSearchParams(url.search).get('visible')
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters]
const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',')
url.searchParams.set('visible', settings)
window.history.pushState({}, null, unescape(url.href))
}
const showCategory = (categoryToShow) => {
if (typeof window === 'undefined') {
return
}
const url = new URL(window.location.href)
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',').filter(Boolean) ||
[...possibleFilters]
const settings = [...new Set([categoryToShow, ...currentVisible])]
const noFilter = possibleFilters.length === settings.length || !settings.length
noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(','))
window.history.pushState({}, null, unescape(url.href))
}
const getSort = (initialSort) => {
const url = new URL(window.location.href)
let sort = new URLSearchParams(url.search).get('sort')
if (!sort) {
sort = initialSort || 'result'
}
return sort
}
const setSort = (type) => {
const url = new URL(window.location.href)
url.searchParams.set('sort', type)
window.history.pushState({}, null, unescape(url.href))
}
const getCollapsedCategory = (renderCollapsed) => {
let categories
if (typeof window !== 'undefined') {
const url = new URL(window.location.href)
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
switch (true) {
case !renderCollapsed && collapsedItems === null:
categories = ['passed']
break
case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems):
categories = []
break
case /^all$/.test(collapsedItems) || collapsedItems === null && /^all$/.test(renderCollapsed):
categories = [...possibleFilters]
break
default:
categories = collapsedItems?.split(',').map((item) => item.toLowerCase()) || renderCollapsed
break
}
} else {
categories = []
}
return categories
}
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) || false
const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending)
const getCollapsedIds = () => JSON.parse(sessionStorage.getItem('collapsedIds')) || []
const setCollapsedIds = (list) => sessionStorage.setItem('collapsedIds', JSON.stringify(list))
module.exports = {
getVisible,
hideCategory,
showCategory,
getCollapsedIds,
setCollapsedIds,
getSort,
setSort,
getSortDirection,
setSortDirection,
getCollapsedCategory,
possibleFilters,
}
},{}]},{},[4]);
</script>
</footer>
</html>

View file

@ -0,0 +1,770 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title id="head-title">test_authentik_blueprint_api.html</title>
<link href="assets\style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<h1 id="title">test_authentik_blueprint_api.html</h1>
<p>Report generated on 09-Dec-2023 at 12:22:45 by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a>
v4.1.1</p>
<div id="environment-header">
<h2>Environment</h2>
</div>
<table id="environment"></table>
<!-- TEMPLATES -->
<template id="template_environment_row">
<tr>
<td></td>
<td></td>
</tr>
</template>
<template id="template_results-table__body--empty">
<tbody class="results-table-row">
<tr id="not-found-message">
<td colspan="4">No results found. Check the filters.</th>
</tr>
</template>
<template id="template_results-table__tbody">
<tbody class="results-table-row">
<tr class="collapsible">
</tr>
<tr class="extras-row">
<td class="extra" colspan="4">
<div class="extraHTML"></div>
<div class="media">
<div class="media-container">
<div class="media-container__nav--left"><</div>
<div class="media-container__viewport">
<img src="" />
<video controls>
<source src="" type="video/mp4">
</video>
</div>
<div class="media-container__nav--right">></div>
</div>
<div class="media__name"></div>
<div class="media__counter"></div>
</div>
<div class="logwrapper">
<div class="logexpander"></div>
<div class="log"></div>
</div>
</td>
</tr>
</tbody>
</template>
<!-- END TEMPLATES -->
<div class="summary">
<div class="summary__data">
<h2>Summary</h2>
<div class="additional-summary prefix">
</div>
<p class="run-count">1 test took 00:00:01.</p>
<p class="filter">(Un)check the boxes to filter the results.</p>
<div class="summary__reload">
<div class="summary__reload__button hidden" onclick="location.reload()">
<div>There are still tests running. <br />Reload this page to get the latest results!</div>
</div>
</div>
<div class="summary__spacer"></div>
<div class="controls">
<div class="filters">
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="failed" />
<span class="failed">1 Failed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="passed" disabled/>
<span class="passed">0 Passed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="skipped" disabled/>
<span class="skipped">0 Skipped,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xfailed" disabled/>
<span class="xfailed">0 Expected failures,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xpassed" disabled/>
<span class="xpassed">0 Unexpected passes,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="error" disabled/>
<span class="error">0 Errors,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="rerun" disabled/>
<span class="rerun">0 Reruns</span>
</div>
<div class="collapse">
<button id="show_all_details">Show all details</button>&nbsp;/&nbsp;<button id="hide_all_details">Hide all details</button>
</div>
</div>
</div>
<div class="additional-summary summary">
</div>
<div class="additional-summary postfix">
</div>
</div>
<table id="results-table">
<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>
</table>
</body>
<footer>
<div id="data-container" data-jsonblob="{&#34;environment&#34;: {&#34;Python&#34;: &#34;3.10.13&#34;, &#34;Platform&#34;: &#34;Windows-10-10.0.19045-SP0&#34;, &#34;Packages&#34;: {&#34;pytest&#34;: &#34;7.4.3&#34;, &#34;pluggy&#34;: &#34;1.3.0&#34;}, &#34;Plugins&#34;: {&#34;abra&#34;: &#34;0.2.0&#34;, &#34;base-url&#34;: &#34;2.0.0&#34;, &#34;html&#34;: &#34;4.1.1&#34;, &#34;metadata&#34;: &#34;3.0.0&#34;, &#34;playwright&#34;: &#34;0.4.3&#34;}}, &#34;tests&#34;: {&#34;recipes/authentik/tests_authentik/test_authentik_blueprint_api.py::test_should_create_bug_report&#34;: [{&#34;extras&#34;: [{&#34;name&#34;: &#34;Authentik Blueprint Status&#34;, &#34;format_type&#34;: &#34;text&#34;, &#34;content&#34;: &#34;assets\\recipes_authentik_tests_authentik_test_authentik_blueprint_api.py__test_should_create_bug_report_0_0.txt&#34;, &#34;mime_type&#34;: &#34;text/plain&#34;, &#34;extension&#34;: &#34;txt&#34;}], &#34;result&#34;: &#34;Failed&#34;, &#34;testId&#34;: &#34;recipes/authentik/tests_authentik/test_authentik_blueprint_api.py::test_should_create_bug_report&#34;, &#34;duration&#34;: &#34;00:00:01&#34;, &#34;resultsTableRow&#34;: [&#34;&lt;td class=\&#34;col-result\&#34;&gt;Failed&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-testId\&#34;&gt;recipes/authentik/tests_authentik/test_authentik_blueprint_api.py::test_should_create_bug_report&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-duration\&#34;&gt;00:00:01&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-links\&#34;&gt;&lt;a target=\&#34;_blank\&#34; href=\&#34;assets\\recipes_authentik_tests_authentik_test_authentik_blueprint_api.py__test_should_create_bug_report_0_0.txt\&#34; class=\&#34;col-links__extra text\&#34;&gt;Authentik Blueprint Status&lt;/a&gt;&lt;/td&gt;&#34;], &#34;log&#34;: &#34;api_request_context = &amp;lt;playwright._impl._fetch.APIRequestContext object at 0x0000016EF237C0A0&amp;gt;\nURL = BaseUrl(netloc=&amp;#x27;login.test.dev.local-it.cloud&amp;#x27;, scheme=&amp;#x27;https&amp;#x27;, path=&amp;#x27;&amp;#x27;, params=&amp;#x27;&amp;#x27;, query=&amp;#x27;&amp;#x27;, fragment=&amp;#x27;&amp;#x27;)\nextras = [{&amp;#x27;content&amp;#x27;: &amp;#x27;name enabled status\\n---------------------------... True successful&amp;#x27;, &amp;#x27;extension&amp;#x27;: &amp;#x27;txt&amp;#x27;, &amp;#x27;format_type&amp;#x27;: &amp;#x27;text&amp;#x27;, &amp;#x27;mime_type&amp;#x27;: &amp;#x27;text/plain&amp;#x27;, ...}]\n\n def test_should_create_bug_report(\n api_request_context: APIRequestContext,\n URL: BaseUrl,\n extras,\n ) -&amp;gt; None:\n blueprints = api_request_context.get(URL.get(&amp;quot;api/v3/managed/blueprints&amp;quot;))\n assert blueprints.ok\n blueprints_data = blueprints.json()\n ic(blueprints_data)\n \n # fake failed blueprint\n blueprints_data[&amp;quot;results&amp;quot;][10][&amp;quot;status&amp;quot;] = &amp;quot;failed&amp;quot;\n \n table_data_all = []\n table_data_failed = []\n for item in blueprints_data[&amp;quot;results&amp;quot;]:\n row = [item[&amp;quot;name&amp;quot;], item[&amp;quot;enabled&amp;quot;], item[&amp;quot;status&amp;quot;]]\n table_data_all.append(row)\n if item[&amp;quot;status&amp;quot;] != &amp;quot;successful&amp;quot;:\n table_data_failed.append(row)\n \n table = tabulate(table_data_all, headers=[&amp;quot;name&amp;quot;, &amp;quot;enabled&amp;quot;, &amp;quot;status&amp;quot;])\n extras.append(pytest_html.extras.text(table, name=&amp;quot;Authentik Blueprint Status&amp;quot;))\n \n&amp;gt; assert (\n table_data_failed == []\n ), &amp;quot;One or more blueprints were not successful. See Authentik Blueprint Status in html report&amp;quot;\nE AssertionError: One or more blueprints were not successful. See Authentik Blueprint Status in html report\nE assert [[&amp;#x27;Default - ...ue, &amp;#x27;failed&amp;#x27;]] == []\nE Left contains one more item: [&amp;#x27;Default - Provider authorization flow (explicit consent)&amp;#x27;, True, &amp;#x27;failed&amp;#x27;]\nE Full diff:\nE - []\nE + [[&amp;#x27;Default - Provider authorization flow (explicit consent)&amp;#x27;, True, &amp;#x27;failed&amp;#x27;]]\n\nrecipes\\authentik\\tests_authentik\\test_authentik_blueprint_api.py:36: AssertionError\n&#34;}]}, &#34;renderCollapsed&#34;: [&#34;passed&#34;], &#34;initialSort&#34;: &#34;result&#34;, &#34;title&#34;: &#34;test_authentik_blueprint_api.html&#34;}"></div>
<script>
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const { getCollapsedCategory, setCollapsedIds } = require('./storage.js')
class DataManager {
setManager(data) {
const collapsedCategories = [...getCollapsedCategory(data.renderCollapsed)]
const collapsedIds = []
const tests = Object.values(data.tests).flat().map((test, index) => {
const collapsed = collapsedCategories.includes(test.result.toLowerCase())
const id = `test_${index}`
if (collapsed) {
collapsedIds.push(id)
}
return {
...test,
id,
collapsed,
}
})
const dataBlob = { ...data, tests }
this.data = { ...dataBlob }
this.renderData = { ...dataBlob }
setCollapsedIds(collapsedIds)
}
get allData() {
return { ...this.data }
}
resetRender() {
this.renderData = { ...this.data }
}
setRender(data) {
this.renderData.tests = [...data]
}
toggleCollapsedItem(id) {
this.renderData.tests = this.renderData.tests.map((test) =>
test.id === id ? { ...test, collapsed: !test.collapsed } : test,
)
}
set allCollapsed(collapsed) {
this.renderData = { ...this.renderData, tests: [...this.renderData.tests.map((test) => (
{ ...test, collapsed }
))] }
}
get testSubset() {
return [...this.renderData.tests]
}
get environment() {
return this.renderData.environment
}
get initialSort() {
return this.data.initialSort
}
}
module.exports = {
manager: new DataManager(),
}
},{"./storage.js":8}],2:[function(require,module,exports){
const mediaViewer = require('./mediaviewer.js')
const templateEnvRow = document.getElementById('template_environment_row')
const templateResult = document.getElementById('template_results-table__tbody')
function htmlToElements(html) {
const temp = document.createElement('template')
temp.innerHTML = html
return temp.content.childNodes
}
const find = (selector, elem) => {
if (!elem) {
elem = document
}
return elem.querySelector(selector)
}
const findAll = (selector, elem) => {
if (!elem) {
elem = document
}
return [...elem.querySelectorAll(selector)]
}
const dom = {
getStaticRow: (key, value) => {
const envRow = templateEnvRow.content.cloneNode(true)
const isObj = typeof value === 'object' && value !== null
const values = isObj ? Object.keys(value).map((k) => `${k}: ${value[k]}`) : null
const valuesElement = htmlToElements(
values ? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>` : `<div>${value}</div>`)[0]
const td = findAll('td', envRow)
td[0].textContent = key
td[1].appendChild(valuesElement)
return envRow
},
getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => {
const resultBody = templateResult.content.cloneNode(true)
resultBody.querySelector('tbody').classList.add(result.toLowerCase())
resultBody.querySelector('tbody').id = testId
resultBody.querySelector('.collapsible').dataset.id = id
resultsTableRow.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
resultBody.querySelector('.collapsible').appendChild(t.content)
})
if (log) {
// Wrap lines starting with "E" with span.error to color those lines red
const wrappedLog = log.replace(/^E.*$/gm, (match) => `<span class="error">${match}</span>`)
resultBody.querySelector('.log').innerHTML = wrappedLog
} else {
resultBody.querySelector('.log').remove()
}
if (collapsed) {
resultBody.querySelector('.collapsible > td')?.classList.add('collapsed')
resultBody.querySelector('.extras-row').classList.add('hidden')
} else {
resultBody.querySelector('.collapsible > td')?.classList.remove('collapsed')
}
const media = []
extras?.forEach(({ name, format_type, content }) => {
if (['image', 'video'].includes(format_type)) {
media.push({ path: content, name, format_type })
}
if (format_type === 'html') {
resultBody.querySelector('.extraHTML').insertAdjacentHTML('beforeend', `<div>${content}</div>`)
}
})
mediaViewer.setup(resultBody, media)
// Add custom html from the pytest_html_results_table_html hook
tableHtml?.forEach((item) => {
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
})
return resultBody
},
}
module.exports = {
dom,
htmlToElements,
find,
findAll,
}
},{"./mediaviewer.js":6}],3:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const storageModule = require('./storage.js')
const getFilteredSubSet = (filter) =>
manager.allData.tests.filter(({ result }) => filter.includes(result.toLowerCase()))
const doInitFilter = () => {
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
}
const doFilter = (type, show) => {
if (show) {
storageModule.showCategory(type)
} else {
storageModule.hideCategory(type)
}
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
const sortColumn = storageModule.getSort()
doSort(sortColumn, true)
}
module.exports = {
doFilter,
doInitFilter,
}
},{"./datamanager.js":1,"./sort.js":7,"./storage.js":8}],4:[function(require,module,exports){
const { redraw, bindEvents, renderStatic } = require('./main.js')
const { doInitFilter } = require('./filter.js')
const { doInitSort } = require('./sort.js')
const { manager } = require('./datamanager.js')
const data = JSON.parse(document.getElementById('data-container').dataset.jsonblob)
function init() {
manager.setManager(data)
doInitFilter()
doInitSort()
renderStatic()
redraw()
bindEvents()
}
init()
},{"./datamanager.js":1,"./filter.js":3,"./main.js":5,"./sort.js":7}],5:[function(require,module,exports){
const { dom, find, findAll } = require('./dom.js')
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const { doFilter } = require('./filter.js')
const {
getVisible,
getCollapsedIds,
setCollapsedIds,
getSort,
getSortDirection,
possibleFilters,
} = require('./storage.js')
const removeChildren = (node) => {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
const renderStatic = () => {
const renderEnvironmentTable = () => {
const environment = manager.environment
const rows = Object.keys(environment).map((key) => dom.getStaticRow(key, environment[key]))
const table = document.getElementById('environment')
removeChildren(table)
rows.forEach((row) => table.appendChild(row))
}
renderEnvironmentTable()
}
const addItemToggleListener = (elem) => {
elem.addEventListener('click', ({ target }) => {
const id = target.parentElement.dataset.id
manager.toggleCollapsedItem(id)
const collapsedIds = getCollapsedIds()
if (collapsedIds.includes(id)) {
const updated = collapsedIds.filter((item) => item !== id)
setCollapsedIds(updated)
} else {
collapsedIds.push(id)
setCollapsedIds(collapsedIds)
}
redraw()
})
}
const renderContent = (tests) => {
const sortAttr = getSort(manager.initialSort)
const sortAsc = JSON.parse(getSortDirection())
const rows = tests.map(dom.getResultTBody)
const table = document.getElementById('results-table')
const tableHeader = document.getElementById('results-table-head')
const newTable = document.createElement('table')
newTable.id = 'results-table'
// remove all sorting classes and set the relevant
findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc'))
tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc')
newTable.appendChild(tableHeader)
if (!rows.length) {
const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true)
newTable.appendChild(emptyTable)
} else {
rows.forEach((row) => {
if (!!row) {
findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener)
find('.logexpander', row).addEventListener('click',
(evt) => evt.target.parentNode.classList.toggle('expanded'),
)
newTable.appendChild(row)
}
})
}
table.replaceWith(newTable)
}
const renderDerived = () => {
const currentFilter = getVisible()
possibleFilters.forEach((result) => {
const input = document.querySelector(`input[data-test-result="${result}"]`)
input.checked = currentFilter.includes(result)
})
}
const bindEvents = () => {
const filterColumn = (evt) => {
const { target: element } = evt
const { testResult } = element.dataset
doFilter(testResult, element.checked)
const collapsedIds = getCollapsedIds()
const updated = manager.renderData.tests.map((test) => {
return {
...test,
collapsed: collapsedIds.includes(test.id),
}
})
manager.setRender(updated)
redraw()
}
const header = document.getElementById('environment-header')
header.addEventListener('click', () => {
const table = document.getElementById('environment')
table.classList.toggle('hidden')
header.classList.toggle('collapsed')
})
findAll('input[name="filter_checkbox"]').forEach((elem) => {
elem.addEventListener('click', filterColumn)
})
findAll('.sortable').forEach((elem) => {
elem.addEventListener('click', (evt) => {
const { target: element } = evt
const { columnType } = element.dataset
doSort(columnType)
redraw()
})
})
document.getElementById('show_all_details').addEventListener('click', () => {
manager.allCollapsed = false
setCollapsedIds([])
redraw()
})
document.getElementById('hide_all_details').addEventListener('click', () => {
manager.allCollapsed = true
const allIds = manager.renderData.tests.map((test) => test.id)
setCollapsedIds(allIds)
redraw()
})
}
const redraw = () => {
const { testSubset } = manager
renderContent(testSubset)
renderDerived()
}
module.exports = {
redraw,
bindEvents,
renderStatic,
}
},{"./datamanager.js":1,"./dom.js":2,"./filter.js":3,"./sort.js":7,"./storage.js":8}],6:[function(require,module,exports){
class MediaViewer {
constructor(assets) {
this.assets = assets
this.index = 0
}
nextActive() {
this.index = this.index === this.assets.length - 1 ? 0 : this.index + 1
return [this.activeFile, this.index]
}
prevActive() {
this.index = this.index === 0 ? this.assets.length - 1 : this.index -1
return [this.activeFile, this.index]
}
get currentIndex() {
return this.index
}
get activeFile() {
return this.assets[this.index]
}
}
const setup = (resultBody, assets) => {
if (!assets.length) {
resultBody.querySelector('.media').classList.add('hidden')
return
}
const mediaViewer = new MediaViewer(assets)
const container = resultBody.querySelector('.media-container')
const leftArrow = resultBody.querySelector('.media-container__nav--left')
const rightArrow = resultBody.querySelector('.media-container__nav--right')
const mediaName = resultBody.querySelector('.media__name')
const counter = resultBody.querySelector('.media__counter')
const imageEl = resultBody.querySelector('img')
const sourceEl = resultBody.querySelector('source')
const videoEl = resultBody.querySelector('video')
const setImg = (media, index) => {
if (media?.format_type === 'image') {
imageEl.src = media.path
imageEl.classList.remove('hidden')
videoEl.classList.add('hidden')
} else if (media?.format_type === 'video') {
sourceEl.src = media.path
videoEl.classList.remove('hidden')
imageEl.classList.add('hidden')
}
mediaName.innerText = media?.name
counter.innerText = `${index + 1} / ${assets.length}`
}
setImg(mediaViewer.activeFile, mediaViewer.currentIndex)
const moveLeft = () => {
const [media, index] = mediaViewer.prevActive()
setImg(media, index)
}
const doRight = () => {
const [media, index] = mediaViewer.nextActive()
setImg(media, index)
}
const openImg = () => {
window.open(mediaViewer.activeFile.path, '_blank')
}
if (assets.length === 1) {
container.classList.add('media-container--fullscreen')
} else {
leftArrow.addEventListener('click', moveLeft)
rightArrow.addEventListener('click', doRight)
}
imageEl.addEventListener('click', openImg)
}
module.exports = {
setup,
}
},{}],7:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const storageModule = require('./storage.js')
const genericSort = (list, key, ascending, customOrder) => {
let sorted
if (customOrder) {
sorted = list.sort((a, b) => {
const aValue = a.result.toLowerCase()
const bValue = b.result.toLowerCase()
const aIndex = customOrder.findIndex((item) => item.toLowerCase() === aValue)
const bIndex = customOrder.findIndex((item) => item.toLowerCase() === bValue)
// Compare the indices to determine the sort order
return aIndex - bIndex
})
} else {
sorted = list.sort((a, b) => a[key] === b[key] ? 0 : a[key] > b[key] ? 1 : -1)
}
if (ascending) {
sorted.reverse()
}
return sorted
}
const durationSort = (list, ascending) => {
const parseDuration = (duration) => {
if (duration.includes(':')) {
// If it's in the format "HH:mm:ss"
const [hours, minutes, seconds] = duration.split(':').map(Number)
return (hours * 3600 + minutes * 60 + seconds) * 1000
} else {
// If it's in the format "nnn ms"
return parseInt(duration)
}
}
const sorted = list.sort((a, b) => parseDuration(a['duration']) - parseDuration(b['duration']))
if (ascending) {
sorted.reverse()
}
return sorted
}
const doInitSort = () => {
const type = storageModule.getSort(manager.initialSort)
const ascending = storageModule.getSortDirection()
const list = manager.testSubset
const initialOrder = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed', 'Skipped', 'Passed']
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
if (type?.toLowerCase() === 'original') {
manager.setRender(list)
} else {
let sortedList
switch (type) {
case 'duration':
sortedList = durationSort(list, ascending)
break
case 'result':
sortedList = genericSort(list, type, ascending, initialOrder)
break
default:
sortedList = genericSort(list, type, ascending)
break
}
manager.setRender(sortedList)
}
}
const doSort = (type, skipDirection) => {
const newSortType = storageModule.getSort(manager.initialSort) !== type
const currentAsc = storageModule.getSortDirection()
let ascending
if (skipDirection) {
ascending = currentAsc
} else {
ascending = newSortType ? false : !currentAsc
}
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
const list = manager.testSubset
const sortedList = type === 'duration' ? durationSort(list, ascending) : genericSort(list, type, ascending)
manager.setRender(sortedList)
}
module.exports = {
doInitSort,
doSort,
}
},{"./datamanager.js":1,"./storage.js":8}],8:[function(require,module,exports){
const possibleFilters = [
'passed',
'skipped',
'failed',
'error',
'xfailed',
'xpassed',
'rerun',
]
const getVisible = () => {
const url = new URL(window.location.href)
const settings = new URLSearchParams(url.search).get('visible')
const lower = (item) => {
const lowerItem = item.toLowerCase()
if (possibleFilters.includes(lowerItem)) {
return lowerItem
}
return null
}
return settings === null ?
possibleFilters :
[...new Set(settings?.split(',').map(lower).filter((item) => item))]
}
const hideCategory = (categoryToHide) => {
const url = new URL(window.location.href)
const visibleParams = new URLSearchParams(url.search).get('visible')
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters]
const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',')
url.searchParams.set('visible', settings)
window.history.pushState({}, null, unescape(url.href))
}
const showCategory = (categoryToShow) => {
if (typeof window === 'undefined') {
return
}
const url = new URL(window.location.href)
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',').filter(Boolean) ||
[...possibleFilters]
const settings = [...new Set([categoryToShow, ...currentVisible])]
const noFilter = possibleFilters.length === settings.length || !settings.length
noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(','))
window.history.pushState({}, null, unescape(url.href))
}
const getSort = (initialSort) => {
const url = new URL(window.location.href)
let sort = new URLSearchParams(url.search).get('sort')
if (!sort) {
sort = initialSort || 'result'
}
return sort
}
const setSort = (type) => {
const url = new URL(window.location.href)
url.searchParams.set('sort', type)
window.history.pushState({}, null, unescape(url.href))
}
const getCollapsedCategory = (renderCollapsed) => {
let categories
if (typeof window !== 'undefined') {
const url = new URL(window.location.href)
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
switch (true) {
case !renderCollapsed && collapsedItems === null:
categories = ['passed']
break
case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems):
categories = []
break
case /^all$/.test(collapsedItems) || collapsedItems === null && /^all$/.test(renderCollapsed):
categories = [...possibleFilters]
break
default:
categories = collapsedItems?.split(',').map((item) => item.toLowerCase()) || renderCollapsed
break
}
} else {
categories = []
}
return categories
}
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) || false
const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending)
const getCollapsedIds = () => JSON.parse(sessionStorage.getItem('collapsedIds')) || []
const setCollapsedIds = (list) => sessionStorage.setItem('collapsedIds', JSON.stringify(list))
module.exports = {
getVisible,
hideCategory,
showCategory,
getCollapsedIds,
setCollapsedIds,
getSort,
setSort,
getSortDirection,
setSortDirection,
getCollapsedCategory,
possibleFilters,
}
},{}]},{},[4]);
</script>
</footer>
</html>

View file

@ -0,0 +1,770 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title id="head-title">test_wordpress_receive_email.html</title>
<link href="assets\style.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<h1 id="title">test_wordpress_receive_email.html</h1>
<p>Report generated on 08-Dec-2023 at 16:00:41 by <a href="https://pypi.python.org/pypi/pytest-html">pytest-html</a>
v4.1.1</p>
<div id="environment-header">
<h2>Environment</h2>
</div>
<table id="environment"></table>
<!-- TEMPLATES -->
<template id="template_environment_row">
<tr>
<td></td>
<td></td>
</tr>
</template>
<template id="template_results-table__body--empty">
<tbody class="results-table-row">
<tr id="not-found-message">
<td colspan="4">No results found. Check the filters.</th>
</tr>
</template>
<template id="template_results-table__tbody">
<tbody class="results-table-row">
<tr class="collapsible">
</tr>
<tr class="extras-row">
<td class="extra" colspan="4">
<div class="extraHTML"></div>
<div class="media">
<div class="media-container">
<div class="media-container__nav--left"><</div>
<div class="media-container__viewport">
<img src="" />
<video controls>
<source src="" type="video/mp4">
</video>
</div>
<div class="media-container__nav--right">></div>
</div>
<div class="media__name"></div>
<div class="media__counter"></div>
</div>
<div class="logwrapper">
<div class="logexpander"></div>
<div class="log"></div>
</div>
</td>
</tr>
</tbody>
</template>
<!-- END TEMPLATES -->
<div class="summary">
<div class="summary__data">
<h2>Summary</h2>
<div class="additional-summary prefix">
</div>
<p class="run-count">1 test took 946 ms.</p>
<p class="filter">(Un)check the boxes to filter the results.</p>
<div class="summary__reload">
<div class="summary__reload__button hidden" onclick="location.reload()">
<div>There are still tests running. <br />Reload this page to get the latest results!</div>
</div>
</div>
<div class="summary__spacer"></div>
<div class="controls">
<div class="filters">
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="failed" />
<span class="failed">1 Failed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="passed" disabled/>
<span class="passed">0 Passed,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="skipped" disabled/>
<span class="skipped">0 Skipped,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xfailed" disabled/>
<span class="xfailed">0 Expected failures,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="xpassed" disabled/>
<span class="xpassed">0 Unexpected passes,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="error" disabled/>
<span class="error">0 Errors,</span>
<input checked="true" class="filter" name="filter_checkbox" type="checkbox" data-test-result="rerun" disabled/>
<span class="rerun">0 Reruns</span>
</div>
<div class="collapse">
<button id="show_all_details">Show all details</button>&nbsp;/&nbsp;<button id="hide_all_details">Hide all details</button>
</div>
</div>
</div>
<div class="additional-summary summary">
</div>
<div class="additional-summary postfix">
</div>
</div>
<table id="results-table">
<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>
</table>
</body>
<footer>
<div id="data-container" data-jsonblob="{&#34;environment&#34;: {&#34;Python&#34;: &#34;3.10.13&#34;, &#34;Platform&#34;: &#34;Windows-10-10.0.19045-SP0&#34;, &#34;Packages&#34;: {&#34;pytest&#34;: &#34;7.4.3&#34;, &#34;pluggy&#34;: &#34;1.3.0&#34;}, &#34;Plugins&#34;: {&#34;abra&#34;: &#34;0.2.0&#34;, &#34;base-url&#34;: &#34;2.0.0&#34;, &#34;html&#34;: &#34;4.1.1&#34;, &#34;metadata&#34;: &#34;3.0.0&#34;, &#34;playwright&#34;: &#34;0.4.3&#34;}}, &#34;tests&#34;: {&#34;recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py::test_demo&#34;: [{&#34;extras&#34;: [], &#34;result&#34;: &#34;Failed&#34;, &#34;testId&#34;: &#34;recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py::test_demo&#34;, &#34;duration&#34;: &#34;946 ms&#34;, &#34;resultsTableRow&#34;: [&#34;&lt;td class=\&#34;col-result\&#34;&gt;Failed&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-testId\&#34;&gt;recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py::test_demo&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-duration\&#34;&gt;946 ms&lt;/td&gt;&#34;, &#34;&lt;td class=\&#34;col-links\&#34;&gt;&lt;/td&gt;&#34;], &#34;log&#34;: &#34;imap_recent_messages = [{&amp;#x27;raw_email&amp;#x27;: &amp;#x27;Return-Path: &amp;lt;test@local-it.org&amp;gt;\\r\\nDelivered-To: test@local-it.org\\r\\nReceived: from mail.local-it.or...Fwg@mail.gmail.com&amp;gt;&amp;#x27;, &amp;#x27;subject&amp;#x27;: &amp;#x27;fourth mail&amp;#x27;, &amp;#x27;parsed_date&amp;#x27;: datetime.datetime(2023, 12, 4, 23, 40, 8), &amp;#x27;flags&amp;#x27;: []}]\n\n def test_demo(imap_recent_messages: list[Message]):\n for message in imap_recent_messages:\n print(dir(message))\n ic(message.subject)\n ic(message.body[&amp;quot;plain&amp;quot;])\n \n&amp;gt; exit()\n\nrecipes\\wordpress\\tests_wordpress\\test_wordpress_receive_email.py:12: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = Use exit() or Ctrl-Z plus Return to exit, code = None\n\n def __call__(self, code=None):\n # Shells like IDLE catch the SystemExit, but listen when their\n # stdin wrapper is closed.\n try:\n sys.stdin.close()\n except:\n pass\n&amp;gt; raise SystemExit(code)\nE SystemExit: None\n\nC:\\Users\\Daniel\\anaconda3\\envs\\play\\lib\\_sitebuiltins.py:26: SystemExit\n&#34;}]}, &#34;renderCollapsed&#34;: [&#34;passed&#34;], &#34;initialSort&#34;: &#34;result&#34;, &#34;title&#34;: &#34;test_wordpress_receive_email.html&#34;}"></div>
<script>
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const { getCollapsedCategory, setCollapsedIds } = require('./storage.js')
class DataManager {
setManager(data) {
const collapsedCategories = [...getCollapsedCategory(data.renderCollapsed)]
const collapsedIds = []
const tests = Object.values(data.tests).flat().map((test, index) => {
const collapsed = collapsedCategories.includes(test.result.toLowerCase())
const id = `test_${index}`
if (collapsed) {
collapsedIds.push(id)
}
return {
...test,
id,
collapsed,
}
})
const dataBlob = { ...data, tests }
this.data = { ...dataBlob }
this.renderData = { ...dataBlob }
setCollapsedIds(collapsedIds)
}
get allData() {
return { ...this.data }
}
resetRender() {
this.renderData = { ...this.data }
}
setRender(data) {
this.renderData.tests = [...data]
}
toggleCollapsedItem(id) {
this.renderData.tests = this.renderData.tests.map((test) =>
test.id === id ? { ...test, collapsed: !test.collapsed } : test,
)
}
set allCollapsed(collapsed) {
this.renderData = { ...this.renderData, tests: [...this.renderData.tests.map((test) => (
{ ...test, collapsed }
))] }
}
get testSubset() {
return [...this.renderData.tests]
}
get environment() {
return this.renderData.environment
}
get initialSort() {
return this.data.initialSort
}
}
module.exports = {
manager: new DataManager(),
}
},{"./storage.js":8}],2:[function(require,module,exports){
const mediaViewer = require('./mediaviewer.js')
const templateEnvRow = document.getElementById('template_environment_row')
const templateResult = document.getElementById('template_results-table__tbody')
function htmlToElements(html) {
const temp = document.createElement('template')
temp.innerHTML = html
return temp.content.childNodes
}
const find = (selector, elem) => {
if (!elem) {
elem = document
}
return elem.querySelector(selector)
}
const findAll = (selector, elem) => {
if (!elem) {
elem = document
}
return [...elem.querySelectorAll(selector)]
}
const dom = {
getStaticRow: (key, value) => {
const envRow = templateEnvRow.content.cloneNode(true)
const isObj = typeof value === 'object' && value !== null
const values = isObj ? Object.keys(value).map((k) => `${k}: ${value[k]}`) : null
const valuesElement = htmlToElements(
values ? `<ul>${values.map((val) => `<li>${val}</li>`).join('')}<ul>` : `<div>${value}</div>`)[0]
const td = findAll('td', envRow)
td[0].textContent = key
td[1].appendChild(valuesElement)
return envRow
},
getResultTBody: ({ testId, id, log, extras, resultsTableRow, tableHtml, result, collapsed }) => {
const resultBody = templateResult.content.cloneNode(true)
resultBody.querySelector('tbody').classList.add(result.toLowerCase())
resultBody.querySelector('tbody').id = testId
resultBody.querySelector('.collapsible').dataset.id = id
resultsTableRow.forEach((html) => {
const t = document.createElement('template')
t.innerHTML = html
resultBody.querySelector('.collapsible').appendChild(t.content)
})
if (log) {
// Wrap lines starting with "E" with span.error to color those lines red
const wrappedLog = log.replace(/^E.*$/gm, (match) => `<span class="error">${match}</span>`)
resultBody.querySelector('.log').innerHTML = wrappedLog
} else {
resultBody.querySelector('.log').remove()
}
if (collapsed) {
resultBody.querySelector('.collapsible > td')?.classList.add('collapsed')
resultBody.querySelector('.extras-row').classList.add('hidden')
} else {
resultBody.querySelector('.collapsible > td')?.classList.remove('collapsed')
}
const media = []
extras?.forEach(({ name, format_type, content }) => {
if (['image', 'video'].includes(format_type)) {
media.push({ path: content, name, format_type })
}
if (format_type === 'html') {
resultBody.querySelector('.extraHTML').insertAdjacentHTML('beforeend', `<div>${content}</div>`)
}
})
mediaViewer.setup(resultBody, media)
// Add custom html from the pytest_html_results_table_html hook
tableHtml?.forEach((item) => {
resultBody.querySelector('td[class="extra"]').insertAdjacentHTML('beforeend', item)
})
return resultBody
},
}
module.exports = {
dom,
htmlToElements,
find,
findAll,
}
},{"./mediaviewer.js":6}],3:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const storageModule = require('./storage.js')
const getFilteredSubSet = (filter) =>
manager.allData.tests.filter(({ result }) => filter.includes(result.toLowerCase()))
const doInitFilter = () => {
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
}
const doFilter = (type, show) => {
if (show) {
storageModule.showCategory(type)
} else {
storageModule.hideCategory(type)
}
const currentFilter = storageModule.getVisible()
const filteredSubset = getFilteredSubSet(currentFilter)
manager.setRender(filteredSubset)
const sortColumn = storageModule.getSort()
doSort(sortColumn, true)
}
module.exports = {
doFilter,
doInitFilter,
}
},{"./datamanager.js":1,"./sort.js":7,"./storage.js":8}],4:[function(require,module,exports){
const { redraw, bindEvents, renderStatic } = require('./main.js')
const { doInitFilter } = require('./filter.js')
const { doInitSort } = require('./sort.js')
const { manager } = require('./datamanager.js')
const data = JSON.parse(document.getElementById('data-container').dataset.jsonblob)
function init() {
manager.setManager(data)
doInitFilter()
doInitSort()
renderStatic()
redraw()
bindEvents()
}
init()
},{"./datamanager.js":1,"./filter.js":3,"./main.js":5,"./sort.js":7}],5:[function(require,module,exports){
const { dom, find, findAll } = require('./dom.js')
const { manager } = require('./datamanager.js')
const { doSort } = require('./sort.js')
const { doFilter } = require('./filter.js')
const {
getVisible,
getCollapsedIds,
setCollapsedIds,
getSort,
getSortDirection,
possibleFilters,
} = require('./storage.js')
const removeChildren = (node) => {
while (node.firstChild) {
node.removeChild(node.firstChild)
}
}
const renderStatic = () => {
const renderEnvironmentTable = () => {
const environment = manager.environment
const rows = Object.keys(environment).map((key) => dom.getStaticRow(key, environment[key]))
const table = document.getElementById('environment')
removeChildren(table)
rows.forEach((row) => table.appendChild(row))
}
renderEnvironmentTable()
}
const addItemToggleListener = (elem) => {
elem.addEventListener('click', ({ target }) => {
const id = target.parentElement.dataset.id
manager.toggleCollapsedItem(id)
const collapsedIds = getCollapsedIds()
if (collapsedIds.includes(id)) {
const updated = collapsedIds.filter((item) => item !== id)
setCollapsedIds(updated)
} else {
collapsedIds.push(id)
setCollapsedIds(collapsedIds)
}
redraw()
})
}
const renderContent = (tests) => {
const sortAttr = getSort(manager.initialSort)
const sortAsc = JSON.parse(getSortDirection())
const rows = tests.map(dom.getResultTBody)
const table = document.getElementById('results-table')
const tableHeader = document.getElementById('results-table-head')
const newTable = document.createElement('table')
newTable.id = 'results-table'
// remove all sorting classes and set the relevant
findAll('.sortable', tableHeader).forEach((elem) => elem.classList.remove('asc', 'desc'))
tableHeader.querySelector(`.sortable[data-column-type="${sortAttr}"]`)?.classList.add(sortAsc ? 'desc' : 'asc')
newTable.appendChild(tableHeader)
if (!rows.length) {
const emptyTable = document.getElementById('template_results-table__body--empty').content.cloneNode(true)
newTable.appendChild(emptyTable)
} else {
rows.forEach((row) => {
if (!!row) {
findAll('.collapsible td:not(.col-links', row).forEach(addItemToggleListener)
find('.logexpander', row).addEventListener('click',
(evt) => evt.target.parentNode.classList.toggle('expanded'),
)
newTable.appendChild(row)
}
})
}
table.replaceWith(newTable)
}
const renderDerived = () => {
const currentFilter = getVisible()
possibleFilters.forEach((result) => {
const input = document.querySelector(`input[data-test-result="${result}"]`)
input.checked = currentFilter.includes(result)
})
}
const bindEvents = () => {
const filterColumn = (evt) => {
const { target: element } = evt
const { testResult } = element.dataset
doFilter(testResult, element.checked)
const collapsedIds = getCollapsedIds()
const updated = manager.renderData.tests.map((test) => {
return {
...test,
collapsed: collapsedIds.includes(test.id),
}
})
manager.setRender(updated)
redraw()
}
const header = document.getElementById('environment-header')
header.addEventListener('click', () => {
const table = document.getElementById('environment')
table.classList.toggle('hidden')
header.classList.toggle('collapsed')
})
findAll('input[name="filter_checkbox"]').forEach((elem) => {
elem.addEventListener('click', filterColumn)
})
findAll('.sortable').forEach((elem) => {
elem.addEventListener('click', (evt) => {
const { target: element } = evt
const { columnType } = element.dataset
doSort(columnType)
redraw()
})
})
document.getElementById('show_all_details').addEventListener('click', () => {
manager.allCollapsed = false
setCollapsedIds([])
redraw()
})
document.getElementById('hide_all_details').addEventListener('click', () => {
manager.allCollapsed = true
const allIds = manager.renderData.tests.map((test) => test.id)
setCollapsedIds(allIds)
redraw()
})
}
const redraw = () => {
const { testSubset } = manager
renderContent(testSubset)
renderDerived()
}
module.exports = {
redraw,
bindEvents,
renderStatic,
}
},{"./datamanager.js":1,"./dom.js":2,"./filter.js":3,"./sort.js":7,"./storage.js":8}],6:[function(require,module,exports){
class MediaViewer {
constructor(assets) {
this.assets = assets
this.index = 0
}
nextActive() {
this.index = this.index === this.assets.length - 1 ? 0 : this.index + 1
return [this.activeFile, this.index]
}
prevActive() {
this.index = this.index === 0 ? this.assets.length - 1 : this.index -1
return [this.activeFile, this.index]
}
get currentIndex() {
return this.index
}
get activeFile() {
return this.assets[this.index]
}
}
const setup = (resultBody, assets) => {
if (!assets.length) {
resultBody.querySelector('.media').classList.add('hidden')
return
}
const mediaViewer = new MediaViewer(assets)
const container = resultBody.querySelector('.media-container')
const leftArrow = resultBody.querySelector('.media-container__nav--left')
const rightArrow = resultBody.querySelector('.media-container__nav--right')
const mediaName = resultBody.querySelector('.media__name')
const counter = resultBody.querySelector('.media__counter')
const imageEl = resultBody.querySelector('img')
const sourceEl = resultBody.querySelector('source')
const videoEl = resultBody.querySelector('video')
const setImg = (media, index) => {
if (media?.format_type === 'image') {
imageEl.src = media.path
imageEl.classList.remove('hidden')
videoEl.classList.add('hidden')
} else if (media?.format_type === 'video') {
sourceEl.src = media.path
videoEl.classList.remove('hidden')
imageEl.classList.add('hidden')
}
mediaName.innerText = media?.name
counter.innerText = `${index + 1} / ${assets.length}`
}
setImg(mediaViewer.activeFile, mediaViewer.currentIndex)
const moveLeft = () => {
const [media, index] = mediaViewer.prevActive()
setImg(media, index)
}
const doRight = () => {
const [media, index] = mediaViewer.nextActive()
setImg(media, index)
}
const openImg = () => {
window.open(mediaViewer.activeFile.path, '_blank')
}
if (assets.length === 1) {
container.classList.add('media-container--fullscreen')
} else {
leftArrow.addEventListener('click', moveLeft)
rightArrow.addEventListener('click', doRight)
}
imageEl.addEventListener('click', openImg)
}
module.exports = {
setup,
}
},{}],7:[function(require,module,exports){
const { manager } = require('./datamanager.js')
const storageModule = require('./storage.js')
const genericSort = (list, key, ascending, customOrder) => {
let sorted
if (customOrder) {
sorted = list.sort((a, b) => {
const aValue = a.result.toLowerCase()
const bValue = b.result.toLowerCase()
const aIndex = customOrder.findIndex((item) => item.toLowerCase() === aValue)
const bIndex = customOrder.findIndex((item) => item.toLowerCase() === bValue)
// Compare the indices to determine the sort order
return aIndex - bIndex
})
} else {
sorted = list.sort((a, b) => a[key] === b[key] ? 0 : a[key] > b[key] ? 1 : -1)
}
if (ascending) {
sorted.reverse()
}
return sorted
}
const durationSort = (list, ascending) => {
const parseDuration = (duration) => {
if (duration.includes(':')) {
// If it's in the format "HH:mm:ss"
const [hours, minutes, seconds] = duration.split(':').map(Number)
return (hours * 3600 + minutes * 60 + seconds) * 1000
} else {
// If it's in the format "nnn ms"
return parseInt(duration)
}
}
const sorted = list.sort((a, b) => parseDuration(a['duration']) - parseDuration(b['duration']))
if (ascending) {
sorted.reverse()
}
return sorted
}
const doInitSort = () => {
const type = storageModule.getSort(manager.initialSort)
const ascending = storageModule.getSortDirection()
const list = manager.testSubset
const initialOrder = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed', 'Skipped', 'Passed']
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
if (type?.toLowerCase() === 'original') {
manager.setRender(list)
} else {
let sortedList
switch (type) {
case 'duration':
sortedList = durationSort(list, ascending)
break
case 'result':
sortedList = genericSort(list, type, ascending, initialOrder)
break
default:
sortedList = genericSort(list, type, ascending)
break
}
manager.setRender(sortedList)
}
}
const doSort = (type, skipDirection) => {
const newSortType = storageModule.getSort(manager.initialSort) !== type
const currentAsc = storageModule.getSortDirection()
let ascending
if (skipDirection) {
ascending = currentAsc
} else {
ascending = newSortType ? false : !currentAsc
}
storageModule.setSort(type)
storageModule.setSortDirection(ascending)
const list = manager.testSubset
const sortedList = type === 'duration' ? durationSort(list, ascending) : genericSort(list, type, ascending)
manager.setRender(sortedList)
}
module.exports = {
doInitSort,
doSort,
}
},{"./datamanager.js":1,"./storage.js":8}],8:[function(require,module,exports){
const possibleFilters = [
'passed',
'skipped',
'failed',
'error',
'xfailed',
'xpassed',
'rerun',
]
const getVisible = () => {
const url = new URL(window.location.href)
const settings = new URLSearchParams(url.search).get('visible')
const lower = (item) => {
const lowerItem = item.toLowerCase()
if (possibleFilters.includes(lowerItem)) {
return lowerItem
}
return null
}
return settings === null ?
possibleFilters :
[...new Set(settings?.split(',').map(lower).filter((item) => item))]
}
const hideCategory = (categoryToHide) => {
const url = new URL(window.location.href)
const visibleParams = new URLSearchParams(url.search).get('visible')
const currentVisible = visibleParams ? visibleParams.split(',') : [...possibleFilters]
const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',')
url.searchParams.set('visible', settings)
window.history.pushState({}, null, unescape(url.href))
}
const showCategory = (categoryToShow) => {
if (typeof window === 'undefined') {
return
}
const url = new URL(window.location.href)
const currentVisible = new URLSearchParams(url.search).get('visible')?.split(',').filter(Boolean) ||
[...possibleFilters]
const settings = [...new Set([categoryToShow, ...currentVisible])]
const noFilter = possibleFilters.length === settings.length || !settings.length
noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(','))
window.history.pushState({}, null, unescape(url.href))
}
const getSort = (initialSort) => {
const url = new URL(window.location.href)
let sort = new URLSearchParams(url.search).get('sort')
if (!sort) {
sort = initialSort || 'result'
}
return sort
}
const setSort = (type) => {
const url = new URL(window.location.href)
url.searchParams.set('sort', type)
window.history.pushState({}, null, unescape(url.href))
}
const getCollapsedCategory = (renderCollapsed) => {
let categories
if (typeof window !== 'undefined') {
const url = new URL(window.location.href)
const collapsedItems = new URLSearchParams(url.search).get('collapsed')
switch (true) {
case !renderCollapsed && collapsedItems === null:
categories = ['passed']
break
case collapsedItems?.length === 0 || /^["']{2}$/.test(collapsedItems):
categories = []
break
case /^all$/.test(collapsedItems) || collapsedItems === null && /^all$/.test(renderCollapsed):
categories = [...possibleFilters]
break
default:
categories = collapsedItems?.split(',').map((item) => item.toLowerCase()) || renderCollapsed
break
}
} else {
categories = []
}
return categories
}
const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) || false
const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending)
const getCollapsedIds = () => JSON.parse(sessionStorage.getItem('collapsedIds')) || []
const setCollapsedIds = (list) => sessionStorage.setItem('collapsedIds', JSON.stringify(list))
module.exports = {
getVisible,
hideCategory,
showCategory,
getCollapsedIds,
setCollapsedIds,
getSort,
setSort,
getSortDirection,
setSortDirection,
getCollapsedCategory,
possibleFilters,
}
},{}]},{},[4]);
</script>
</footer>
</html>

60
tests/test_html_merge.py Normal file
View file

@ -0,0 +1,60 @@
# tmp_path fixture:
# https://docs.pytest.org/en/6.2.x/tmpdir.html
from pathlib import Path
import pytest
from icecream import ic # type: ignore
from playwright.sync_api import BrowserContext, expect
from pytest_abra import BaseUrl
from pytest_abra.html_helper import merge_html_reports
@pytest.fixture(scope="session")
def session_tmp_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
return tmp_path_factory.mktemp("html_test")
def test_merge_html(session_tmp_path: Path):
"""combines all generated pytest html reports into one"""
in_dir_path = Path(__file__).parent / "assets" / "html_merge"
in_dir_path = in_dir_path.resolve()
ic(in_dir_path)
out_file_path = session_tmp_path / "test.html"
out_assets_dir = session_tmp_path / "assets"
merge_html_reports(in_dir_path.as_posix(), out_file_path.as_posix(), "combined.html")
assert out_file_path.is_file()
assert out_assets_dir.is_dir()
assert next(out_assets_dir.glob("*"))
@pytest.mark.slow
def test_check_result_with_playwright(session_tmp_path, context: BrowserContext):
html_file = session_tmp_path / "test.html"
file_url = BaseUrl(netloc=html_file.as_posix(), scheme="file").get()
page = context.new_page()
page.goto(file_url)
# check if combined is correct
expect(page.get_by_text("2 Passed,")).to_be_visible()
expect(page.get_by_text("2 Failed,")).to_be_visible()
expect(page.get_by_text("tests ran in 12.946 seconds")).to_be_visible()
# check if heading is correct
expect(page.get_by_role("heading", name="combined.html")).to_be_visible()
# check if traceback is included
expect(page.get_by_text("E AssertionError: One or more")).to_be_visible()
# check if asset works
with page.expect_popup() as page1_info:
page.get_by_role("link", name="Authentik Blueprint Status").click()
page1 = page1_info.value
# see if content of txt file is correct
expect(page1.get_by_text("failed")).to_be_visible()