Issues list: the #481 comment-bubble svg mounts EMPTY (no path child) on most rows #491
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#491
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issues list: the #481 comment-bubble svg mounts EMPTY (no path child) on most rows — only the most-recently-rendered row gets its glyph
What's broken
On the issues list, the right-aligned comment-count indicator renders an
<svg class="icon" viewBox="0 0 16 16">with no children for most rows — the bubble glyph is invisible, only the count text shows. The empty mount is row-order-dependent: in a controlled reproduction the last-rendered row is the only one whose svg contains its<path>; every row rendered before it mounts an empty svg. The pill icons on the repo header (watch/star/fork/clone, plus) render their paths fine on the same page load — the failure is specific to the per-row<Icon name="issue-comment" />inpages/Issues.jsx.Introduced by the #481 fix (commit
02057e3, bundleindex-zFgw6znv.js, solid-js 1.9.15, vite-plugin-solid).Reproduction (controlled, no server needed)
Serve
web/dist(vite build of current main), mock/api/v1/*with an 8-issue list (comment_count0–7), load/crueber/walhub/issuesin headless Chromium. Survey every svg on the page:Instrumenting the page confirms the path node is never removed after mount (
MutationObserverover the whole document sees zero removals; notextContentwrites hit any row svg). The path for the earlier rows is simply never inserted into the svg that reaches the DOM — consistent with the icon body being composed into the B-cached template node rather than the live clone for all but the final render. No console errors.Root cause (static analysis, marked as such)
web/src/lib/icons.jsxbuilds every icon from ONE shared svg shape:na= the shared<svg xmlns width=1em height=1em aria-hidden>(compiled by vite-plugin-solid into atemplate()clone cache), andbodyJSX (also compiled into per-entry template clones), whichIconinserts imperatively:W(n, () => t().body)composes the body into a clone of the shared svg template at runtime. When this Icon renders once per<For>row, the body insertion only lands in the DOM for the most-recently-created icon instance; earlier rows keep the empty shared-svg clone. The repo-header pills work because each renders exactly once in a static shell, not per-item inside<For>.The row call site is
web/src/pages/Issues.jsx:357(<Icon name="issue-comment" /> {issue.comment_count}), inside the per-issue<For>.Requested fix
Replace the shared-template + imperative-body-insert mechanism in
web/src/lib/icons.jsxwith the pattern the codebase already uses inweb/src/components/Empty.jsx: each ICONS entry stores the complete inline<svg>JSX (ownxmlns,width/height 1em,viewBox,aria-hidden="true",class), andIconrenders{ICONS[props.name]}directly — no shared shape, no runtimeW()composition, no template-clone interaction with Solid's insert logic. Keep: currentColor-only paint, no fetch/innerHTML,ICON_NAMES, unknown-name-renders-nothing behavior, and the#465/#466/#481header attribution. All call sites (Repo.jsx,NotificationTray.jsx,App.jsx,CreateMenu.jsx,Issues.jsx) keep their<Icon name=… />usage unchanged.Acceptance criteria
childElementCount >= 1for all rows, any list length/order) — verify in a rendered browser, not only source-text testsissue-comment-481.test.js/icons-465.test.jspins updated to the new entry shape (full-svg per entry), and a DOM-level test added so a per-row empty mount can't regress silently againFix PR: #493 (branch fix/issue-491). Per-entry factory svgs per the prescribed fix; all path data byte-identical; call sites unchanged. Tests: new icons-per-row-491.test.js (structural regression pins + SSR per-row For harness, 8/8 green) + four pin-test updates; full node suite 1081/1079/2 with the 2 pre-existing smoke failures identical on pristine main; vite build green. Rendered-browser verification explicitly open (shared-daemon loopback guard).
REVIEW of PR #493 (fix/issue-491, commit
d9846fb+1cbdb48reviewer nit-fix) — verified in scratch worktree /tmp/pr493, main worktree untouched.(1) Per-entry factories — PASS. web/src/lib/icons.jsx: all 15 entries are (cls) => (<svg ...>) factories, each a complete svg with own xmlns/width/height 1em/shipped viewBox/aria-hidden/class={cls}. No shared template, no .body map, no viewBox={ dynamic composition, no imperative insert.
(2) Path data byte-identical — PASS, verified programmatically: 17 d-attributes extracted from origin/main vs PR branch, ordered-equal (Counter-equal and list-equal). ViewBox sequence identical (16/16/24/24/1200/1024x4/24/16/16/16/24/24).
(3) Call sites unchanged — PASS. Zero diff under web/src/pages, web/src/components, App.jsx; Issues.jsx:364 still .
(4) Incidental fixes sound — PASS. (a) entry()(cls()) correctly unwraps the non-keyed-Show accessor (same idiom as old e().viewBox). (b) clone fragment unwrapped to two direct s, zero <> left; vite build green proves the dom compiler accepts it.
(5) SSR harness validity — KEY QUESTION, answered empirically: new test run against OLD icons.jsx gives 5 pass / 3 fail. The 3 structural tests (no-shared-shape, factory regression pin, header) FAIL on old code = genuine regression guard. But the SSR per-row scenario, 15-name sweep, and unknown-name tests PASS on old code too — SSR stringifies, so the harness CANNOT exhibit the client move-not-clone hazard and does NOT exercise the mount path that was broken. The test file's own honesty note admits this. Rendered-browser verification (childElementCount on rows, acceptance criterion #1) remains explicitly OPEN — noted, not blocking given the structural pins, but a real-browser drive is still owed before/after merge if a drive becomes available.
(6) Pins faithful — PASS. Four updated files change only shape spellings (viewBox: " -> viewBox=", 1->15 svg counts, slice bound }, -> ),, bodies->entries).
(7) No new deps / docs — PASS. package.json untouched (runtime deps still solid-js/@solidjs/router/marked/dompurify); babel pair resolves through declared vite-plugin-solid devDependency. Law-12 decision appended to docs/go/12_web_ui.md. Laws 1/7/8/12 hold.
TESTS (scratch): node --test web/test/unit/*.test.js = 1081 total / 1079 pass / 2 fail — the 2 are smoke.test.js live-server subtests fetching http://127.0.0.1:8080 (a live instance is up in this env; failures are 401/infra-shape, definitionally independent of icons.jsx). vite build green with the issue-comment path in the bundle.
REVIEWER FIX PUSHED (
1cbdb48): removed dead const os = require(node:os) from icons-per-row-491.test.js (unused, unique in suite); file re-tested 8/8 green after.RECOMMENDATION: ready to merge (with browser verification recorded as open follow-up, as the PR itself states).
Fixed by PR #493 (review clean — per-entry factories, byte-identical paths, pins faithful; SSR-harness limits honestly noted, browser verification open), merged. Closing.