m6/no-scroll: stacked cards on mobile, tables on desktop

817a536gl0w

all commits

Message

m6/no-scroll: stacked cards on mobile, tables on desktop

Diff

src/build.rs+32 −7
diff --git a/src/build.rs b/src/build.rsindex 70aa5d9..96c819f 100644--- a/src/build.rs+++ b/src/build.rs@@ -584,8 +584,8 @@ pub fn file_table_html(584584     );585585     if let Some((href, label)) = parent {586586         out.push_str(&format!(587-            "<tr class=\"parent\"><td><a href=\"{}\">{}</a></td>\588-             <td></td><td></td></tr>\n",587+            "<tr class=\"parent\"><td data-label=\"Name\"><a href=\"{}\">{}</a></td>\588+             <td data-label=\"Last commit\"></td><td data-label=\"Age\"></td></tr>\n",589589             esc(href),590590             esc(label)591591         ));@@ -600,8 +600,8 @@ pub fn file_table_html(600600         let msg_short = truncate_msg(&msg);601601         if e.is_dir {602602             out.push_str(&format!(603-                "<tr class=\"dir\"><td><a href=\"/files/{}.html\">{}/</a></td>\604-                 <td class=\"title\">{}</td><td>{}</td></tr>\n",603+                "<tr class=\"dir\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}/</a></td>\604+                 <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",605605                 href_esc(&full),606606                 esc(&e.name),607607                 esc(&msg_short),@@ -609,8 +609,8 @@ pub fn file_table_html(609609             ));610610         } else {611611             out.push_str(&format!(612-                "<tr class=\"file\"><td><a href=\"/files/{}.html\">{}</a></td>\613-                 <td class=\"title\">{}</td><td>{}</td></tr>\n",612+                "<tr class=\"file\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}</a></td>\613+                 <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",614614                 href_esc(&full),615615                 esc(&e.name),616616                 esc(&msg_short),@@ -1682,7 +1682,32 @@ mod tests {16821682             "breadcrumb kept on subdir"16831683         );16841684         // Message + age columns exist even when history is empty-ish.1685-        assert!(sub.contains("<td class=\"title\">"), "message cell");1685+        assert!(sub.contains("<td class=\"title\""), "message cell");1686+    }1687+1688+    /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.1689+    #[test]1690+    fn file_table_cells_have_data_labels() {1691+        let css = include_str!("../static/style.css");1692+        assert!(css.contains("content:attr(data-label)"), "label rule");1693+        assert!(css.contains("@media (max-width:767px)"), "narrow query");1694+        let (_repo, out) = build_fixture();1695+        for page in ["index.html", "files/sub.html"] {1696+            let html = std::fs::read_to_string(out.join(page)).expect(page);1697+            let tds = html.matches("<td").count();1698+            assert!(tds > 0, "{page} has cells");1699+            assert_eq!(1700+                html.matches("data-label=").count(),1701+                tds,1702+                "{page}: every td labeled"1703+            );1704+            for label in ["Name", "Last commit", "Age"] {1705+                assert!(1706+                    html.contains(&format!("data-label=\"{label}\"")),1707+                    "{page}: label {label}"1708+                );1709+            }1710+        }16861711     }16871712 16881713     #[test]
src/commits.rs+31 −3
diff --git a/src/commits.rs b/src/commits.rsindex d5fa33e..7075113 100644--- a/src/commits.rs+++ b/src/commits.rs@@ -40,7 +40,7 @@ pub fn render_loaded(4040         body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>sha</th><th>subject</th><th>author</th><th>date</th></tr></thead>\n<tbody>\n");4141         for c in log {4242             body.push_str(&format!(43-                "<tr><td class=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\">{}</td><td>{}</td><td>{}</td></tr>\n",43+                "<tr><td class=\"sha\" data-label=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\" data-label=\"subject\">{}</td><td data-label=\"author\">{}</td><td data-label=\"date\">{}</td></tr>\n",4444                 esc(&c.sha),4545                 esc(short_sha(&c.sha)),4646                 esc(&c.subject),@@ -342,8 +342,8 @@ mod tests {342342             list.contains("<div class=\"table-scroll\"><table>"),343343             "wrapper"344344         );345-        assert!(list.contains("<td class=\"title\">"), "title cell");346-        assert!(list.contains("<td class=\"sha\">"), "sha cell");345+        assert!(list.contains("<td class=\"title\""), "title cell");346+        assert!(list.contains("<td class=\"sha\""), "sha cell");347347         assert!(list.contains(&long_subject), "long subject kept");348348         let sha = shas(&dir)[0].clone();349349         assert!(@@ -368,4 +368,32 @@ mod tests {368368         let list = std::fs::read_to_string(out.join("commits.html")).expect("list");369369         assert!(list.contains("No commits yet."));370370     }371+372+    /// m6/no-scroll (f0rge #39): commit rows carry data-labels for cards.373+    #[test]374+    fn commit_cells_have_data_labels() {375+        let css = include_str!("../static/style.css");376+        assert!(css.contains("content:attr(data-label)"), "label rule");377+        assert!(css.contains("@media (max-width:767px)"), "narrow query");378+        let dir = mktemp("labels");379+        git(&dir, &["init", "-b", "main"]);380+        git(&dir, &["config", "user.name", "hu13-test"]);381+        git(&dir, &["config", "user.email", "[email protected]"]);382+        git(&dir, &["config", "commit.gpgsign", "false"]);383+        std::fs::write(dir.join("x.txt"), "x\n").unwrap();384+        git(&dir, &["add", "-A"]);385+        git(&dir, &["commit", "-m", "labeled subject"]);386+        let out = mktemp("out");387+        render(&dir, &out, "main", 100, "test", "<nav>", None).expect("render");388+        let list = std::fs::read_to_string(out.join("commits.html")).expect("list");389+        let tds = list.matches("<td").count();390+        assert!(tds > 0, "has cells");391+        assert_eq!(list.matches("data-label=").count(), tds, "every td labeled");392+        for label in ["sha", "subject", "author", "date"] {393+            assert!(394+                list.contains(&format!("data-label=\"{label}\"")),395+                "label {label}"396+            );397+        }398+    }371399 }
src/forge.rs+96 −4
diff --git a/src/forge.rs b/src/forge.rsindex 15280b8..ff7f1cb 100644--- a/src/forge.rs+++ b/src/forge.rs@@ -986,7 +986,7 @@ fn render_issues_index(986986         body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");987987         for issue in &data.issues {988988             body.push_str(&format!(989-                "<tr><td class=\"num\">{n}</td><td class=\"title\"><a href=\"/issues/{id}.html\">{title}</a></td><td>{state}</td><td>{comments}</td><td>{created}</td></tr>\n",989+                "<tr><td class=\"num\" data-label=\"#\">{n}</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{id}.html\">{title}</a></td><td data-label=\"state\">{state}</td><td data-label=\"comments\">{comments}</td><td data-label=\"created\">{created}</td></tr>\n",990990                 n = issue.number,991991                 id = esc(&issue.id),992992                 title = esc(&issue.title),@@ -1045,7 +1045,7 @@ fn render_pulls_index(10451045             let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));10461046             let draft = if pr.draft { " draft" } else { "" };10471047             body.push_str(&format!(1048-                "<tr><td class=\"num\">{n}</td><td class=\"title\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td>{state}</td><td><code>{lanes}</code></td><td>{created}</td></tr>\n",1048+                "<tr><td class=\"num\" data-label=\"#\">{n}</td><td class=\"title\" data-label=\"title\"><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td data-label=\"state\">{state}</td><td data-label=\"head→base\"><code>{lanes}</code></td><td data-label=\"created\">{created}</td></tr>\n",10491049                 n = pr.number,10501050                 id = esc(&pr.id),10511051                 title = esc(&pr.title),@@ -1143,7 +1143,7 @@ fn render_ci_index(ci: &CiInfo, site_name: &str, nav: &str, description: Option<11431143                 body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>step</th><th>exit</th><th>duration_ms</th></tr></thead>\n<tbody>\n");11441144                 for s in &run.steps {11451145                     body.push_str(&format!(1146-                        "<tr><td><code>{}</code></td><td>{}</td><td>{}</td></tr>\n",1146+                        "<tr><td data-label=\"step\"><code>{}</code></td><td data-label=\"exit\">{}</td><td data-label=\"duration_ms\">{}</td></tr>\n",11471147                         esc(&s.name),11481148                         s.exit,11491149                         s.duration_ms,@@ -1690,7 +1690,7 @@ mod tests {16901690                 html.contains("<div class=\"table-scroll\"><table>"),16911691                 "{page}: wrapper"16921692             );1693-            assert!(html.contains("<td class=\"title\">"), "{page}: title cell");1693+            assert!(html.contains("<td class=\"title\""), "{page}: title cell");16941694             assert!(html.contains(&long_title), "{page}: long title kept");16951695         }16961696         let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");@@ -1750,4 +1750,96 @@ mod tests {17501750         assert!(pulls.contains("1 closed"), "pulls closed total");17511751         assert!(pulls.contains(" draft"), "draft stays marked");17521752     }1753+1754+    /// m6/no-scroll (f0rge #39): stacked-card reflow on narrow, desktop untouched.1755+    /// Research: from knowledge of Gitea mobile (issue/PR lists collapse to1756+    /// title-led cards; file table stacks name + meta) — no live fetch here.1757+    #[test]1758+    fn stacked_cards_no_scroll() {1759+        let css = include_str!("../static/style.css");1760+        assert!(css.contains("@media (max-width:767px)"), "narrow query");1761+        assert!(css.contains("overflow-x:visible"), "no scroll on narrow");1762+        assert!(1763+            css.contains("content:attr(data-label)"),1764+            "label via ::before"1765+        );1766+        assert!(css.contains(".table-scroll td::before"), "label rule");1767+        assert!(1768+            css.contains(".table-scroll td.title::before"),1769+            "title leads card"1770+        );1771+        assert!(1772+            css.contains("display:block") && css.contains("display:flex"),1773+            "card layout"1774+        );1775+        assert!(css.contains("width:max-content"), "desktop width kept");1776+        assert!(1777+            css.contains(".table-scroll th,.table-scroll td") && css.contains("white-space:nowrap"),1778+            "desktop nowrap kept"1779+        );1780+        let repo = mktemp("cards");1781+        write_forge(1782+            &repo,1783+            "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",1784+            "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: Card issue\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",1785+        );1786+        write_forge(1787+            &repo,1788+            "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",1789+            "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: Card pr\nstate: open\nbase: main\nhead: feat\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",1790+        );1791+        write_forge(1792+            &repo,1793+            "tasks/cccccccc-cccc-cccc-cccc-cccccccccccc.md",1794+            "---\nid: cccccccc-cccc-cccc-cccc-cccccccccccc\ntitle: Card task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nwork\n",1795+        );1796+        write_forge(&repo, "ci/ci.env", "runner: local\n");1797+        std::fs::create_dir_all(repo.join(".forge").join("ci/steps")).unwrap();1798+        std::fs::write(1799+            repo.join(".forge").join("ci/steps/00_test.sh"),1800+            "#!/bin/sh\n",1801+        )1802+        .unwrap();1803+        write_forge(1804+            &repo,1805+            "ci/runs/2026-09-10T00-00-00Z/summary.md",1806+            "---\nrun: 2026-09-10T00-00-00Z\nverdict: pass\ncreated: 2026-09-10T00:00:00Z\nsteps:\n  - name: 00_test.sh\n    exit: 0\n    duration_ms: 7\n---\nok\n",1807+        );1808+        let out = mktemp("cards-out");1809+        render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");1810+        for (page, labels) in [1811+            (1812+                "issues/index.html",1813+                vec!["#", "title", "state", "comments", "created"],1814+            ),1815+            (1816+                "pulls/index.html",1817+                vec!["#", "title", "state", "head→base", "created"],1818+            ),1819+            ("ci/index.html", vec!["step", "exit", "duration_ms"]),1820+        ] {1821+            let html = std::fs::read_to_string(out.join(page)).expect(page);1822+            let tds = html.matches("<td").count();1823+            assert!(tds > 0, "{page} has cells");1824+            assert_eq!(1825+                html.matches("data-label=").count(),1826+                tds,1827+                "{page}: every td labeled"1828+            );1829+            for label in labels {1830+                assert!(1831+                    html.contains(&format!("data-label=\"{label}\"")),1832+                    "{page}: label {label}"1833+                );1834+            }1835+        }1836+        let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");1837+        assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");1838+        assert!(tasks.contains("todo (1)"), "tasks group kept");1839+        let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");1840+        assert!(1841+            issues.contains("open") && issues.contains("2026-09-10"),1842+            "issues data kept in DOM"1843+        );1844+    }17531845 }
src/releases.rs+44 −2
diff --git a/src/releases.rs b/src/releases.rsindex ad238d9..87493bb 100644--- a/src/releases.rs+++ b/src/releases.rs@@ -391,7 +391,7 @@ pub fn render_loaded(391391         body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>version</th><th>title</th><th>created</th><th>artifacts</th></tr></thead>\n<tbody>\n");392392         for rel in releases {393393             body.push_str(&format!(394-                "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\">{t}</td><td>{c}</td><td>{n}</td></tr>\n",394+                "<tr><td data-label=\"version\"><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\" data-label=\"title\">{t}</td><td data-label=\"created\">{c}</td><td data-label=\"artifacts\">{n}</td></tr>\n",395395                 v = esc(&rel.version),396396                 ve = esc(&rel.version),397397                 t = esc(rel.display_title()),@@ -493,7 +493,7 @@ mod tests {493493             index.contains("<div class=\"table-scroll\"><table>"),494494             "wrapper"495495         );496-        assert!(index.contains("<td class=\"title\">"), "title cell");496+        assert!(index.contains("<td class=\"title\""), "title cell");497497         assert!(index.contains(&long_title), "long title kept");498498     }499499 @@ -510,4 +510,46 @@ mod tests {510510         std::fs::create_dir_all(&dir).unwrap();511511         assert!(load(&dir).is_empty());512512     }513+514+    /// m6/no-scroll (f0rge #39): release rows carry data-labels for cards.515+    #[test]516+    fn release_cells_have_data_labels() {517+        let css = include_str!("../static/style.css");518+        assert!(css.contains("content:attr(data-label)"), "label rule");519+        let rel = Release {520+            version: "v1.2.3".to_string(),521+            title: Some("Labeled".to_string()),522+            created: "2026-09-10T03:00:00Z".to_string(),523+            sha: None,524+            body: "notes\n".to_string(),525+            artifacts: Vec::new(),526+        };527+        let repo = std::env::temp_dir().join(format!(528+            "hu13-rel-labels-{}-{}",529+            std::process::id(),530+            std::time::SystemTime::now()531+                .duration_since(std::time::UNIX_EPOCH)532+                .map(|d| d.as_nanos())533+                .unwrap_or(0)534+        ));535+        std::fs::create_dir_all(&repo).unwrap();536+        let out = repo.join("out");537+        std::fs::create_dir_all(&out).unwrap();538+        render_loaded(&[rel], &repo, &out, "test", "<nav>", None).expect("render");539+        let index =540+            std::fs::read_to_string(out.join("releases").join("index.html")).expect("index");541+        let tds = index.matches("<td").count();542+        assert!(tds > 0, "has cells");543+        assert_eq!(544+            index.matches("data-label=").count(),545+            tds,546+            "every td labeled"547+        );548+        for label in ["version", "title", "created", "artifacts"] {549+            assert!(550+                index.contains(&format!("data-label=\"{label}\"")),551+                "label {label}"552+            );553+        }554+    }513555 }
static/style.css+21 −0
diff --git a/static/style.css b/static/style.cssindex a09ecf2..2efee6d 100644--- a/static/style.css+++ b/static/style.css@@ -183,3 +183,24 @@ details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt)183183 details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan)}184184 details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px}185185 .clone-hint,.clonebox summary .muted{font-weight:400}186+/* Stacked cards on narrow (m6/no-scroll, f0rge #39): Gitea-style reflow.187+   Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.188+   Narrow: no scroll (overflow visible), thead clipped for SR only, each189+   tr becomes a bordered card, each td a labeled flex row via data-label.190+   Title cells lead bold without a repeated label; nothing is deleted. */191+@media (max-width:767px){192+  .table-scroll{overflow-x:visible}193+  .table-scroll table,.file-table table{width:100%;min-width:0}194+  .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}195+  .table-scroll tbody,.table-scroll tr{display:block;width:auto}196+  .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}197+  .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}198+  .table-scroll td{display:flex;gap:.5rem;align-items:baseline;padding:.4rem .75rem;border:none;border-bottom:1px solid var(--border);min-width:0;overflow-wrap:anywhere}199+  .table-scroll td:last-child{border-bottom:none}200+  .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}201+  .table-scroll td.title::before{display:none}202+  .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}203+  .table-scroll td.num,.table-scroll td.sha{width:auto}204+  .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}205+  .table-scroll tbody tr:hover td{background:none}206+}
tests/forge_readers.rs+5 −5
diff --git a/tests/forge_readers.rs b/tests/forge_readers.rsindex d695600..b3aa723 100644--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {224224 225225     // --- issues index: 2 rendered rows (broken one skipped) ---226226     let issues = read("issues/index.html");227-    let rows = issues.matches("<tr><td class=\"num\">").count();227+    let rows = issues.matches("<tr><td class=\"num\"").count();228228     assert_eq!(rows, 2, "rendered issues match file-count minus broken");229229     assert!(!issues.contains("No title here"), "broken issue skipped");230230     // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.231231     assert!(232232         issues.contains(&format!(233-            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"233+            "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"234234         )),235235         "issue #1 -> older uuid"236236     );237237     assert!(238238         issues.contains(&format!(239-            "<td class=\"num\">2</td><td class=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"239+            "<td class=\"num\" data-label=\"#\">2</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"240240         )),241241         "issue #2 -> newer uuid"242242     );@@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {263263     // (4): pulls — same shape + head/base/draft/ci + patch <pre>.264264     let pulls = read("pulls/index.html");265265     assert_eq!(266-        pulls.matches("<tr><td class=\"num\">").count(),266+        pulls.matches("<tr><td class=\"num\"").count(),267267         1,268268         "prs match file-count"269269     );270270     assert!(271271         pulls.contains(&format!(272-            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"272+            "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"273273         )),274274         "pr #1 -> uuid"275275     );
tests/releases_api.rs+5 −5
diff --git a/tests/releases_api.rs b/tests/releases_api.rsindex 9ea9a71..ea32b2c 100644--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -110,7 +110,7 @@ fn releases_artifacts_api_agents_feed() {110110 111111     // --- releases: index + both detail pages ---112112     let index = read("releases/index.html");113-    assert_eq!(index.matches("<tr><td>").count(), 2, "two release rows");113+    assert_eq!(index.matches("<tr><td").count(), 2, "two release rows");114114     assert!(index.contains("v1.0"), "v1.0 listed");115115     assert!(index.contains("v2.0"), "v2.0 listed");116116 @@ -154,15 +154,15 @@ fn releases_artifacts_api_agents_feed() {154154 155155     // --- api counts == html counts (same struct pass) ---156156     let issues_html = read("issues/index.html")157-        .matches("<tr><td class=\"num\">")157+        .matches("<tr><td class=\"num\"")158158         .count();159159     let pulls_html = read("pulls/index.html")160-        .matches("<tr><td class=\"num\">")160+        .matches("<tr><td class=\"num\"")161161         .count();162162     let commits_html = read("commits.html")163-        .matches("<tr><td class=\"sha\">")163+        .matches("<tr><td class=\"sha\"")164164         .count();165-    let releases_html = read("releases/index.html").matches("<tr><td>").count();165+    let releases_html = read("releases/index.html").matches("<tr><td").count();166166     let wiki_html = read("wiki/index.html")167167         .matches("<li><a href=\"/wiki/")168168         .count();
tests/responsive.rs+4 −4
diff --git a/tests/responsive.rs b/tests/responsive.rsindex 37b536e..f0c4b2d 100644--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -377,23 +377,23 @@ fn forge_list_tables_scroll_and_wrap_long_titles() {377377     for page in ["issues/index.html", "pulls/index.html"] {378378         let html = read(page);379379         assert!(380-            html.contains("<td class=\"title\">"),380+            html.contains("<td class=\"title\""),381381             "{page} marks title cells"382382         );383383         assert!(384384             html.contains(&long_title),385385             "{page} keeps the full long title"386386         );387-        assert!(html.contains("<td class=\"num\">"), "{page} keeps # narrow");387+        assert!(html.contains("<td class=\"num\""), "{page} keeps # narrow");388388     }389389     let commits = read("commits.html");390390     assert!(391-        commits.contains("<td class=\"sha\">"),391+        commits.contains("<td class=\"sha\""),392392         "commits marks sha cells"393393     );394394     let releases = read("releases/index.html");395395     assert!(396-        releases.contains("<td class=\"title\">"),396+        releases.contains("<td class=\"title\""),397397         "releases marks title cells"398398     );399399     // Labels from the fixture still list on the issues index.