diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 272751a5..e9e3db2d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -403,6 +403,7 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m2-icons-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m3-icons-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m4-icons-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1657-analytics-channels-group-sprites-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/analytics.js b/public/analytics.js index 6769da3a..6ee8487a 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -1033,7 +1033,11 @@ if (!rows.length) continue; parts.push( '' + - esc(sections[si].label) + ' (' + rows.length + ')' + + // sections[].label is a hardcoded sprite-bearing string (no user + // input) — must be inserted raw so the renders. esc() here + // HTML-escaped the angle brackets and surfaced literal "" + // text in the table (#1657). + sections[si].label + ' (' + rows.length + ')' + '' ); for (var ri = 0; ri < rows.length; ri++) parts.push(channelRowHtml(rows[ri])); diff --git a/test-issue-1657-analytics-channels-group-sprites-e2e.js b/test-issue-1657-analytics-channels-group-sprites-e2e.js new file mode 100644 index 00000000..f60cef38 --- /dev/null +++ b/test-issue-1657-analytics-channels-group-sprites-e2e.js @@ -0,0 +1,148 @@ +#!/usr/bin/env node +/* Issue #1657 — Analytics → Channels: group-header rows must render real + * Phosphor sprites (My Channels / Network / Encrypted), NOT the + * HTML-escaped literal "" source text. + * + * Bug: public/analytics.js passed the hardcoded group-section label (which + * contains the sprite markup) through esc(), HTML-encoding the angle + * brackets so the browser displayed the source instead of rendering it. + * + * This test asserts (in real Chromium against a running server): + * (1) The Channel Activity table has at least one group-header row. + * (2) Each rendered group-header row contains a real + * element (NOT escaped text). + * (3) For each expected group (key/radio/lock), if the label text is + * present, an actual resolves + * inside the same row. + * (4) The Channel Activity table's innerText contains zero literal + * " { + const tb = document.getElementById('channelsTbody'); + return tb && tb.querySelectorAll('tr').length > 0; + }, null, { timeout: 12000 }); + } catch { + fail('channelsTbody never populated within 12s'); + await browser.close(); + console.log(`\ntest-issue-1657: ${passes} passed, ${failures} failed`); + process.exit(failures ? 1 : 0); + } + + // Allow grouped render (decorate + buildHashKeyMap promise) to settle. + await page.waitForTimeout(800); + + const result = await page.evaluate(() => { + const tb = document.getElementById('channelsTbody'); + if (!tb) return { error: 'tbody missing' }; + const headerRows = Array.from(tb.querySelectorAll('tr.ch-section-row')); + const perHeader = headerRows.map((tr) => { + const cell = tr.querySelector('td.ch-section-header') || tr.querySelector('td'); + const text = (cell && cell.textContent) || ''; + const svgs = cell ? cell.querySelectorAll('svg.ph-icon').length : 0; + const uses = cell + ? Array.from(cell.querySelectorAll('svg.ph-icon use')) + .map((u) => (u.getAttribute('href') || u.getAttribute('xlink:href') || '')) + : []; + return { text: text.trim(), svgs, uses }; + }); + const tableEl = document.getElementById('channelsTable') || tb.parentElement; + const innerHTML = tableEl ? tableEl.innerHTML : ''; + // innerText reflects what the user sees; literal "`); + + // (3) per expected group, verify the sprite ref + const expected = [ + { match: /My Channels/i, ref: /#ph-key$/, name: 'My Channels → #ph-key' }, + { match: /Network/i, ref: /#ph-radio$/, name: 'Network → #ph-radio' }, + { match: /Encrypted/i, ref: /#ph-lock$/, name: 'Encrypted → #ph-lock' }, + ]; + for (const e of expected) { + const row = result.headerRows.find((h) => e.match.test(h.text)); + if (!row) { + // Not all groups are guaranteed to have rows (depends on fixture data). + // Skip silently — group only renders if it has channels. + continue; + } + const ok = row.uses.some((u) => e.ref.test(u)); + if (!ok) fail(`(3) ${e.name}: expected ${e.ref} but uses=${JSON.stringify(row.uses)}`); + else pass(`(3) ${e.name} rendered as real sprite`); + } + + // (4) no literal " { + console.error('test-issue-1657: FAIL —', err); + process.exit(1); +});