v3.22.0 (fix): an expanded run log stays open across the live report polls

The report view re-renders the member blocks on every poll while a suite runs
(innerHTML replacement), so an expanded Run log collapsed within 500 ms.  The
<details> now carries the stable member index (data-member) and
testRenderReport carries the open state across the re-render; the log content
keeps updating in place.

- frontend/tests/testsuites.test.js: the wiring pin (data-member emitted, the
  open state read and restored).
- sw cache simple-v400 (index.html changed); style.css rebuilt.

Tests: 837 frontend + 795 Python.
This commit is contained in:
2026-10-06 00:50:24 +03:00
parent 22a0a58f27
commit bcd9fb9a4c
4 changed files with 90 additions and 8 deletions
+14 -3
View File
@@ -1430,7 +1430,7 @@
<div id="test-view-report" class="hidden">
<div class="flex flex-wrap items-end gap-2 mb-3">
<button onclick="testViewShow('suites')" class="px-3 py-1.5 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600">&larr; <span data-l10n="Suites">Suites</span></button>
<span id="test-report-title" class="flex-1 text-sm font-medium"></span>
<span id="test-report-title" class="flex-1 text-sm font-medium text-gray-700 dark:text-slate-200"></span>
<button id="test-report-json" onclick="testReportExportJson()" class="px-3 py-1.5 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Export JSON">Export JSON</button>
<button id="test-report-md" onclick="testReportExportMarkdown()" class="px-3 py-1.5 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Export Markdown">Export Markdown</button>
<button onclick="testClearReport()" class="px-3 py-1.5 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Clear report">Clear report</button>
@@ -8295,14 +8295,25 @@ function testRenderReport() {
if (snap.adm && snap.adm.required) parts.push('ADM: ' + (snap.adm.verified ? t('verified') : t('not verified')));
if (snap.error) parts.push(esc(snap.error));
sumEl.innerHTML = esc(parts.join(' · '));
// A live run re-renders this block on every poll: replacing innerHTML
// creates fresh <details>, so an expanded Run log would collapse each
// time. Carry the open state across the re-render (the member list is
// fixed at run start, so the index is a stable key).
const openLogs = {};
memEl.querySelectorAll('details[data-member]').forEach(d => {
openLogs[d.dataset.member] = d.open;
});
memEl.innerHTML = (s.members || []).map(testReportMemberHtml).join('');
memEl.querySelectorAll('details[data-member]').forEach(d => {
if (openLogs[d.dataset.member]) d.open = true;
});
}
function testReportMemberHtml(m, i) {
let html = '<div class="border border-gray-200 dark:border-slate-700 rounded mb-2">'
+ '<div class="flex items-center gap-2 px-2 py-1.5 bg-gray-50 dark:bg-slate-800/50">'
+ '<span class="w-5 text-right text-xs text-gray-400">' + (i + 1) + '</span>'
+ '<span class="flex-1 truncate">' + esc(m.name || m.script_id || '') + '</span>';
+ '<span class="flex-1 truncate text-gray-700 dark:text-slate-200">' + esc(m.name || m.script_id || '') + '</span>';
if (m.role && m.role !== 'member') {
html += '<span class="text-xs px-1.5 py-0.5 rounded bg-gray-200 dark:bg-slate-700">' + esc(t(m.role)) + '</span>';
}
@@ -8311,7 +8322,7 @@ function testReportMemberHtml(m, i) {
if (m.note) html += '<div class="px-2 py-1 text-xs text-red-500">' + esc(m.note) + '</div>';
if ((m.steps || []).length) html += '<div class="px-2 py-1">' + m.steps.map(testRunStepHtml).join('') + '</div>';
if ((m.log || []).length) {
html += '<details class="px-2 pb-1"><summary class="text-xs text-gray-500 cursor-pointer">'
html += '<details class="px-2 pb-1" data-member="' + i + '"><summary class="text-xs text-gray-500 cursor-pointer">'
+ esc(t('Run log')) + (m.log_truncated ? ' (' + esc(t('truncated')) + ')' : '') + '</summary>'
+ '<pre class="text-xs whitespace-pre-wrap break-all text-gray-500 dark:text-slate-400 max-h-64 overflow-auto">'
+ esc(m.log.join('\n')) + '</pre></details>';
+48 -4
View File
@@ -620,6 +620,10 @@ video {
visibility: visible;
}
.collapse {
visibility: collapse;
}
.static {
position: static;
}
@@ -976,12 +980,12 @@ video {
min-width: 0px;
}
.min-w-\[14rem\] {
min-width: 14rem;
.min-w-\[12rem\] {
min-width: 12rem;
}
.min-w-\[16rem\] {
min-width: 16rem;
.min-w-\[14rem\] {
min-width: 14rem;
}
.min-w-\[180px\] {
@@ -1357,6 +1361,11 @@ video {
border-top-color: transparent;
}
.bg-amber-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1));
}
.bg-amber-50 {
--tw-bg-opacity: 1;
background-color: rgb(255 251 235 / var(--tw-bg-opacity, 1));
@@ -1439,6 +1448,11 @@ video {
background-color: rgb(250 250 250 / var(--tw-bg-opacity, 1));
}
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity, 1));
}
.bg-red-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1));
@@ -1571,6 +1585,10 @@ video {
padding-bottom: 0.25rem;
}
.pb-1\.5 {
padding-bottom: 0.375rem;
}
.pb-3 {
padding-bottom: 0.75rem;
}
@@ -1591,6 +1609,10 @@ video {
padding-left: 1.5rem;
}
.pl-7 {
padding-left: 1.75rem;
}
.pr-2 {
padding-right: 0.5rem;
}
@@ -1784,6 +1806,11 @@ video {
color: rgb(147 51 234 / var(--tw-text-opacity, 1));
}
.text-purple-700 {
--tw-text-opacity: 1;
color: rgb(126 34 206 / var(--tw-text-opacity, 1));
}
.text-red-400 {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity, 1));
@@ -2028,6 +2055,10 @@ video {
background-color: rgb(120 53 15 / 0.2);
}
.dark\:bg-amber-900\/40:is(.dark *) {
background-color: rgb(120 53 15 / 0.4);
}
.dark\:bg-blue-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
@@ -2049,6 +2080,10 @@ video {
background-color: rgb(6 78 59 / 0.4);
}
.dark\:bg-purple-900\/40:is(.dark *) {
background-color: rgb(88 28 135 / 0.4);
}
.dark\:bg-red-900\/30:is(.dark *) {
background-color: rgb(127 29 29 / 0.3);
}
@@ -2067,6 +2102,10 @@ video {
background-color: rgb(30 41 59 / 0.4);
}
.dark\:bg-slate-800\/50:is(.dark *) {
background-color: rgb(30 41 59 / 0.5);
}
.dark\:bg-slate-800\/60:is(.dark *) {
background-color: rgb(30 41 59 / 0.6);
}
@@ -2126,6 +2165,11 @@ video {
color: rgb(129 140 248 / var(--tw-text-opacity, 1));
}
.dark\:text-purple-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(216 180 254 / var(--tw-text-opacity, 1));
}
.dark\:text-purple-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity, 1));
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v398';
const CACHE = 'simple-v400';
const URLS = [
'index.html',
'help.html',
+27
View File
@@ -184,3 +184,30 @@ test('role rows cannot be moved and the i18n refresh redraws the test views', ()
const i18n = extractFunc(html, 'refreshDynamicI18n');
assert.match(i18n, /isViewVisible\('phone-sub-test'\)\) testRender\(\)/);
});
test('the report keeps an expanded run log open across the live polls', () => {
// A live suite run re-renders the member blocks on every poll; replacing
// innerHTML creates fresh <details>, so the open state is carried over
// by the stable member index.
const member = extractFunc(html, 'testReportMemberHtml');
assert.match(member, /data-member="/);
const report = extractFunc(html, 'testRenderReport');
assert.match(report, /details\[data-member\]/);
assert.match(report, /openLogs\[d\.dataset\.member\]/);
assert.match(report, /d\.open = true/);
});
test('the report header classes are generated in the prebuilt stylesheet', () => {
// The stale-style trap (v3.22.0 review): style.css is a prebuilt Tailwind
// output - a class used only in the PWA source has no rule until
// `npm run build` runs in frontend/. The report's member header is the
// regression case (a near-white bg + dark-theme text made titles vanish).
const css = fs.readFileSync(path.join(__dirname, '..', 'style.css'), 'utf8');
for (const sel of ['.bg-gray-50', '.dark\\:bg-slate-800\\/50',
'.dark\\:text-slate-200', '.bg-purple-100', '.dark\\:bg-purple-900\\/40',
'.min-w-\\[12rem\\]']) {
assert.ok(css.includes(sel), 'missing rule for ' + sel);
}
assert.match(extractFunc(html, 'testReportMemberHtml'),
/bg-gray-50 dark:bg-slate-800\/50/);
});