What changed, and why it matters
This commit is a test-infrastructure cleanup. It moves JavaScript code out of HTML files into a shared external file, bundles the pixelmatch image-comparison library locally instead of loading it from a CDN, and makes the UI test reports wait for images to finish loading before comparing them. There is no security-relevant change to the Trezor firmware or wallet behavior.
No security action required. Treat as a normal test/reporting refactor.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors Trezor’s UI test reporting HTML generation. Inline scripts and inline onload/onclick handlers are removed from generated HTML; the pixelmatch library is vendored into tests/ui_tests/reporting/testreport.js; event listeners are attached in a central onLoad() function; and getImageData()/imageLoaded() are made async so diffs are computed only after images load. The .gitignore now ignores *.js in the UI tests directory, and the GitHub UI-report action skips renaming testreport.js because it is identical across jobs. No cryptographic, firmware, or device-handling code is modified.
Changed components
tests/ui_tests/reporting/common.pytests/ui_tests/reporting/html.pytests/ui_tests/reporting/testreport.jstests/ui_tests/reporting/testreport.py.github/actions/ui-report/action.ymltests/ui_tests/.gitignoreInspect captured patch +71 / −36
diff --git a/.github/actions/ui-report/action.yml b/.github/actions/ui-report/action.yml
index f572e747d..7d6058c77 100644
--- a/.github/actions/ui-report/action.yml
+++ b/.github/actions/ui-report/action.yml
@@ -33,7 +33,10 @@ runs:
# rename all model/job-speficific report files, so they won't be overwritten during upload
cd $OUTDIR
for F in *; do
- if [ -f "$F" ]; then
+ # skip renaming since it has the same contents for all jobs
+ if [ "$F" = "testreport.js" ]; then
+ echo "Skip $F"
+ elif [ -f "$F" ]; then
mv -v $F $MODELJOB-$F
fi
done
diff --git a/tests/ui_tests/.gitignore b/tests/ui_tests/.gitignore
index 6ff4503ef..c406c7913 100644
--- a/tests/ui_tests/.gitignore
+++ b/tests/ui_tests/.gitignore
@@ -2,6 +2,7 @@
*.html
*.zip
*.txt
+*.js
fixtures.suggestion.json
fixtures.results.json
reporting/master_cache
diff --git a/tests/ui_tests/reporting/common.py b/tests/ui_tests/reporting/common.py
index acd7e13f2..3e69fbedb 100644
--- a/tests/ui_tests/reporting/common.py
+++ b/tests/ui_tests/reporting/common.py
@@ -121,10 +121,7 @@ def document(
doc = dominate.document(title=title)
style = t.style()
style.add_raw_string(STYLE)
- script = t.script()
- script.add_raw_string(GIF_SCRIPT)
- script.add_raw_string(SCRIPT)
- doc.head.add(style, script)
+ doc.head.add(style)
if actual_hash is not None:
doc.body["data-actual-hash"] = actual_hash
@@ -169,9 +166,7 @@ def _create_testcase_html_diff_file(
test_name = test_case.id
doc = document(title=test_name, model=test_case.model)
with doc.head:
- script(
- type="text/javascript", src="https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0"
- )
+ script(type="text/javascript", src="../testreport.js")
with doc:
h1(test_name)
p("This UI test differs from master.", style="color: grey; font-weight: bold;")
@@ -205,9 +200,7 @@ def _differing_screens_report(
doc = document(title="Master differing screens", model=model)
with doc.head:
- script(
- type="text/javascript", src="https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0"
- )
+ script(type="text/javascript", src="testreport.js")
with doc:
with table(border=1, width=600):
with tr():
diff --git a/tests/ui_tests/reporting/html.py b/tests/ui_tests/reporting/html.py
index d96ff5a5b..1cdbb7a92 100644
--- a/tests/ui_tests/reporting/html.py
+++ b/tests/ui_tests/reporting/html.py
@@ -43,11 +43,15 @@ def report_links(
td(a(test.name, href=path))
-def write(fixture_test_path: Path, doc: document, filename: str) -> Path:
- (fixture_test_path / filename).write_text(doc.render())
+def write_raw(fixture_test_path: Path, doc: str, filename: str) -> Path:
+ (fixture_test_path / filename).write_text(doc)
return fixture_test_path / filename
+def write(fixture_test_path: Path, doc: document, filename: str) -> Path:
+ return write_raw(fixture_test_path, doc.render(), filename)
+
+
def image_column(hash: str | None, cur_dir: Path, img_id: str | None = None) -> None:
"""Put image into table as one cell."""
with td():
@@ -89,7 +93,7 @@ def image_link(
src=_relative_path(cur_dir, path),
title=title,
loading="lazy",
- onload="imageLoaded(this)",
+ _class="image-link",
)
@@ -114,7 +118,7 @@ def collapsible_rows(rows, cur_dir):
with tr(bgcolor="yellow"):
with td(colspan=3, _class="showLink"):
span(f"{len(rows)} hidden")
- a("show all", href="#", onclick="return showAllHidden()")
+ a("show all", _class="show-all-hidden", href="#")
def diff_table(diff: Iterable[tuple[str | None, str | None]], cur_dir: Path) -> None:
diff --git a/tests/ui_tests/reporting/testreport.js b/tests/ui_tests/reporting/testreport.js
index 0706390e5..fd16d6582 100644
--- a/tests/ui_tests/reporting/testreport.js
+++ b/tests/ui_tests/reporting/testreport.js
@@ -1,4 +1,6 @@
-
+// BEGIN https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0
+const defaultOptions={threshold:.1,includeAA:!1,alpha:.1,aaColor:[255,255,0],diffColor:[255,0,0],diffColorAlt:null,diffMask:!1};function pixelmatch(t,e,r,n,i,a){if(!isPixelData(t)||!isPixelData(e)||r&&!isPixelData(r))throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");if(t.length!==e.length||r&&r.length!==t.length)throw new Error("Image sizes do not match.");if(t.length!==n*i*4)throw new Error("Image data size does not match width/height.");a=Object.assign({},defaultOptions,a);const l=n*i,o=new Uint32Array(t.buffer,t.byteOffset,l),f=new Uint32Array(e.buffer,e.byteOffset,l);let s=!0;for(let t=0;t<l;t++)if(o[t]!==f[t]){s=!1;break}if(s){if(r&&!a.diffMask)for(let e=0;e<l;e++)drawGrayPixel(t,4*e,a.alpha,r);return 0}const d=35215*a.threshold*a.threshold;let u=0;for(let l=0;l<i;l++)for(let o=0;o<n;o++){const f=4*(l*n+o),s=colorDelta(t,e,f,f);Math.abs(s)>d?a.includeAA||!antialiased(t,o,l,n,i,e)&&!antialiased(e,o,l,n,i,t)?(r&&drawPixel(r,f,...s<0&&a.diffColorAlt||a.diffColor),u++):r&&!a.diffMask&&drawPixel(r,f,...a.aaColor):r&&(a.diffMask||drawGrayPixel(t,f,a.alpha,r))}return u}function isPixelData(t){return ArrayBuffer.isView(t)&&1===t.constructor.BYTES_PER_ELEMENT}function antialiased(t,e,r,n,i,a){const l=Math.max(e-1,0),o=Math.max(r-1,0),f=Math.min(e+1,n-1),s=Math.min(r+1,i-1),d=4*(r*n+e);let u,c,h,b,g=e===l||e===f||r===o||r===s?1:0,x=0,y=0;for(let i=l;i<=f;i++)for(let a=o;a<=s;a++){if(i===e&&a===r)continue;const l=colorDelta(t,t,d,4*(a*n+i),!0);if(0===l){if(g++,g>2)return!1}else l<x?(x=l,u=i,c=a):l>y&&(y=l,h=i,b=a)}return 0!==x&&0!==y&&(hasManySiblings(t,u,c,n,i)&&hasManySiblings(a,u,c,n,i)||hasManySiblings(t,h,b,n,i)&&hasManySiblings(a,h,b,n,i))}function hasManySiblings(t,e,r,n,i){const a=Math.max(e-1,0),l=Math.max(r-1,0),o=Math.min(e+1,n-1),f=Math.min(r+1,i-1),s=4*(r*n+e);let d=e===a||e===o||r===l||r===f?1:0;for(let i=a;i<=o;i++)for(let a=l;a<=f;a++){if(i===e&&a===r)continue;const l=4*(a*n+i);if(t[s]===t[l]&&t[s+1]===t[l+1]&&t[s+2]===t[l+2]&&t[s+3]===t[l+3]&&d++,d>2)return!0}return!1}function colorDelta(t,e,r,n,i){let a=t[r+0],l=t[r+1],o=t[r+2],f=t[r+3],s=e[n+0],d=e[n+1],u=e[n+2],c=e[n+3];if(f===c&&a===s&&l===d&&o===u)return 0;f<255&&(f/=255,a=blend(a,f),l=blend(l,f),o=blend(o,f)),c<255&&(c/=255,s=blend(s,c),d=blend(d,c),u=blend(u,c));const h=rgb2y(a,l,o),b=rgb2y(s,d,u),g=h-b;if(i)return g;const x=rgb2i(a,l,o)-rgb2i(s,d,u),y=rgb2q(a,l,o)-rgb2q(s,d,u),M=.5053*g*g+.299*x*x+.1957*y*y;return h>b?-M:M}function rgb2y(t,e,r){return.29889531*t+.58662247*e+.11448223*r}function rgb2i(t,e,r){return.59597799*t-.2741761*e-.32180189*r}function rgb2q(t,e,r){return.21147017*t-.52261711*e+.31114694*r}function blend(t,e){return 255+(t-255)*e}function drawPixel(t,e,r,n,i){t[e+0]=r,t[e+1]=n,t[e+2]=i,t[e+3]=255}function drawGrayPixel(t,e,r,n){const i=blend(rgb2y(t[e+0],t[e+1],t[e+2]),r*t[e+3]/255);drawPixel(n,e,i,i,i)}
+// END https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0
function refreshMarkStates() {
for (let tr of document.body.querySelectorAll("tr[data-actual-hash]")) {
@@ -139,6 +141,13 @@ function onLoadTestCase() {
}
}
+function onClick(id, handler) {
+ const el = document.getElementById(id);
+ if (el) {
+ el.addEventListener("click", handler);
+ }
+}
+
function onLoad() {
if (window.location.protocol === "file") return
@@ -160,11 +169,35 @@ function onLoad() {
// Uncaught DOMException: Permission denied to access property "document" on cross-origin object
onLoadTestCase()
}
+
+ document.querySelectorAll('a.show-all-hidden').forEach(a => {
+ a.addEventListener("click", function(e) { return showAllHidden() });
+ });
+ document.querySelectorAll('.image-link').forEach(img => {
+ img.addEventListener('load', async function() { await imageLoaded(this) });
+ });
+ onClick('reset-state-all', () => resetState('all'));
+ onClick('reset-state-ok', () => resetState('ok'));
+ onClick('reset-state-bad', () => resetState('bad'));
+ onClick('mark-ok', () => markState('ok'));
+ onClick('mark-update', () => markState('update'));
+ onClick('mark-bad', () => markState('bad'));
}
var module = {};
-function getImageData(image) {
+function waitForImage(image) {
+ return image.complete && image.naturalWidth !== 0
+ ? Promise.resolve()
+ : new Promise((resolve, reject) => {
+ image.onload = resolve;
+ image.onerror = reject;
+ });
+}
+
+async function getImageData(image) {
+ await waitForImage(image);
+
// Get original image size
const width = image.naturalWidth;
const height = image.naturalHeight;
@@ -183,12 +216,12 @@ function getImageData(image) {
}
-function imageLoaded(img) {
+async function imageLoaded(img) {
let row = img.closest("tr");
- createRowDiff(row);
+ await createRowDiff(row);
}
-function createRowDiff(row) {
+async function createRowDiff(row) {
// Find an element with recorded image
recImg = row.querySelector("td:nth-child(1) > img");
// Find an element with the current image
@@ -199,8 +232,8 @@ function createRowDiff(row) {
}
// Get images's raw data
- recData = getImageData(recImg);
- curData = getImageData(curImg);
+ recData = await getImageData(recImg);
+ curData = await getImageData(curImg);
const width = recImg.naturalWidth;
const height = recImg.naturalHeight;
diff --git a/tests/ui_tests/reporting/testreport.py b/tests/ui_tests/reporting/testreport.py
index 0f4e521a1..f720624ad 100644
--- a/tests/ui_tests/reporting/testreport.py
+++ b/tests/ui_tests/reporting/testreport.py
@@ -27,7 +27,9 @@ from dominate.util import text
from ..common import FixturesType, TestCase, TestResult
from . import download, html
from .common import (
+ GIF_SCRIPT,
REPORTS_PATH,
+ SCRIPT,
document,
generate_master_diff_report,
get_diff,
@@ -91,6 +93,8 @@ def index() -> Path:
title = "UI Test report " + datetime.now().strftime("%Y-%m-%d %H:%M:%S")
doc = document(title=title, index=True)
+ with doc.head:
+ script(type="text/javascript", src="testreport.js")
with doc:
h1("UI Test report")
@@ -111,13 +115,13 @@ def index() -> Path:
with t.ul():
with t.li():
t.span("new", style="color: blue")
- t.button("clear all", onclick="resetState('all')")
+ t.button("clear all", id="reset-state-all")
with t.li():
t.span("marked OK", style="color: grey")
- t.button("clear", onclick="resetState('ok')")
+ t.button("clear", id="reset-state-ok")
with t.li():
t.span("marked BAD", style="color: darkred")
- t.button("clear", onclick="resetState('bad')")
+ t.button("clear", id="reset-state-bad")
html.report_links(failed_tests, TESTREPORT_PATH, actual_hashes)
@@ -127,6 +131,7 @@ def index() -> Path:
h2("Passed", style="color: green;")
html.report_links(passed_tests, TESTREPORT_PATH)
+ html.write_raw(TESTREPORT_PATH, GIF_SCRIPT + SCRIPT, "testreport.js")
return html.write(TESTREPORT_PATH, doc, "index.html")
@@ -222,9 +227,7 @@ def differing_screens() -> None:
model = recent_ui_failures[0].test.model if recent_ui_failures else None
doc = document(title="Differing screens", model=model)
with doc.head:
- script(
- type="text/javascript", src="https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0"
- )
+ script(type="text/javascript", src="testreport.js")
with doc:
with table(border=1, width=600):
with tr():
@@ -330,9 +333,7 @@ def failed(result: TestResult) -> Path:
title=result.test.id, actual_hash=result.actual_hash, model=result.test.model
)
with doc.head:
- script(
- type="text/javascript", src="https://cdn.jsdelivr.net/npm/pixelmatch@5.3.0"
- )
+ script(type="text/javascript", src="../testreport.js")
with doc:
@@ -341,9 +342,9 @@ def failed(result: TestResult) -> Path:
with div(id="markbox", _class="script-hidden"):
p("Click a button to mark the test result as:")
with div(id="buttons"):
- t.button("OK", id="mark-ok", onclick="markState('ok')")
- t.button("OK & UPDATE", id="mark-update", onclick="markState('update')")
- t.button("BAD", id="mark-bad", onclick="markState('bad')")
+ t.button("OK", id="mark-ok")
+ t.button("OK & UPDATE", id="mark-update")
+ t.button("BAD", id="mark-bad")
if download_failed:
with p():
@@ -379,7 +380,8 @@ def recorded(result: TestResult, header: str = "Recorded", dir: str = "passed")
_copy_deduplicated(result.test)
doc = document(title=result.test.id, model=result.test.model)
-
+ with doc.head:
+ script(type="text/javascript", src="../testreport.js")
with doc:
_header(result.test.id, result.actual_hash, result.actual_hash)
@@ -392,5 +394,4 @@ def recorded(result: TestResult, header: str = "Recorded", dir: str = "passed")
with tr():
td(index)
html.image_column(screen, TESTREPORT_PATH / dir, img_id=str(index))
-
return html.write(TESTREPORT_PATH / dir, doc, result.test.id + ".html")
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.