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:
+14
-3
@@ -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">← <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
@@ -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
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v398';
|
||||
const CACHE = 'simple-v400';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user