1 From dacca5a820d372be31d9058ab209055921e63e70 Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 08:39:46 +01004 Subject: [PATCH] m6/no-scroll: stacked cards on mobile, tables on desktop5 6 ---7 src/build.rs | 39 +++++++++++++---8 src/commits.rs | 34 ++++++++++++--9 src/forge.rs | 100 +++++++++++++++++++++++++++++++++++++++--10 src/releases.rs | 46 ++++++++++++++++++-11 static/style.css | 21 +++++++++12 tests/forge_readers.rs | 10 ++---13 tests/releases_api.rs | 10 ++---14 tests/responsive.rs | 8 ++--15 8 files changed, 238 insertions(+), 30 deletions(-)16 17 diff --git a/src/build.rs b/src/build.rs18 index d70dc3f..b0b69ca 10064419 --- a/src/build.rs20 +++ b/src/build.rs21 @@ -584,8 +584,8 @@ pub fn file_table_html(22 );23 if let Some((href, label)) = parent {24 out.push_str(&format!(25 - "<tr class=\"parent\"><td><a href=\"{}\">{}</a></td>\26 - <td></td><td></td></tr>\n",27 + "<tr class=\"parent\"><td data-label=\"Name\"><a href=\"{}\">{}</a></td>\28 + <td data-label=\"Last commit\"></td><td data-label=\"Age\"></td></tr>\n",29 esc(href),30 esc(label)31 ));32 @@ -600,8 +600,8 @@ pub fn file_table_html(33 let msg_short = truncate_msg(&msg);34 if e.is_dir {35 out.push_str(&format!(36 - "<tr class=\"dir\"><td><a href=\"/files/{}.html\">{}/</a></td>\37 - <td class=\"title\">{}</td><td>{}</td></tr>\n",38 + "<tr class=\"dir\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}/</a></td>\39 + <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",40 href_esc(&full),41 esc(&e.name),42 esc(&msg_short),43 @@ -609,8 +609,8 @@ pub fn file_table_html(44 ));45 } else {46 out.push_str(&format!(47 - "<tr class=\"file\"><td><a href=\"/files/{}.html\">{}</a></td>\48 - <td class=\"title\">{}</td><td>{}</td></tr>\n",49 + "<tr class=\"file\"><td data-label=\"Name\"><a href=\"/files/{}.html\">{}</a></td>\50 + <td class=\"title\" data-label=\"Last commit\">{}</td><td data-label=\"Age\">{}</td></tr>\n",51 href_esc(&full),52 esc(&e.name),53 esc(&msg_short),54 @@ -1651,7 +1651,32 @@ mod tests {55 "breadcrumb kept on subdir"56 );57 // Message + age columns exist even when history is empty-ish.58 - assert!(sub.contains("<td class=\"title\">"), "message cell");59 + assert!(sub.contains("<td class=\"title\""), "message cell");60 + }61 +62 + /// m6/no-scroll (f0rge #39): file-table rows carry data-labels for cards.63 + #[test]64 + fn file_table_cells_have_data_labels() {65 + let css = include_str!("../static/style.css");66 + assert!(css.contains("content:attr(data-label)"), "label rule");67 + assert!(css.contains("@media (max-width:767px)"), "narrow query");68 + let (_repo, out) = build_fixture();69 + for page in ["index.html", "files/sub.html"] {70 + let html = std::fs::read_to_string(out.join(page)).expect(page);71 + let tds = html.matches("<td").count();72 + assert!(tds > 0, "{page} has cells");73 + assert_eq!(74 + html.matches("data-label=").count(),75 + tds,76 + "{page}: every td labeled"77 + );78 + for label in ["Name", "Last commit", "Age"] {79 + assert!(80 + html.contains(&format!("data-label=\"{label}\"")),81 + "{page}: label {label}"82 + );83 + }84 + }85 }86 87 #[test]88 diff --git a/src/commits.rs b/src/commits.rs89 index d5fa33e..7075113 10064490 --- a/src/commits.rs91 +++ b/src/commits.rs92 @@ -40,7 +40,7 @@ pub fn render_loaded(93 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");94 for c in log {95 body.push_str(&format!(96 - "<tr><td class=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\">{}</td><td>{}</td><td>{}</td></tr>\n",97 + "<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",98 esc(&c.sha),99 esc(short_sha(&c.sha)),100 esc(&c.subject),101 @@ -342,8 +342,8 @@ mod tests {102 list.contains("<div class=\"table-scroll\"><table>"),103 "wrapper"104 );105 - assert!(list.contains("<td class=\"title\">"), "title cell");106 - assert!(list.contains("<td class=\"sha\">"), "sha cell");107 + assert!(list.contains("<td class=\"title\""), "title cell");108 + assert!(list.contains("<td class=\"sha\""), "sha cell");109 assert!(list.contains(&long_subject), "long subject kept");110 let sha = shas(&dir)[0].clone();111 assert!(112 @@ -368,4 +368,32 @@ mod tests {113 let list = std::fs::read_to_string(out.join("commits.html")).expect("list");114 assert!(list.contains("No commits yet."));115 }116 +117 + /// m6/no-scroll (f0rge #39): commit rows carry data-labels for cards.118 + #[test]119 + fn commit_cells_have_data_labels() {120 + let css = include_str!("../static/style.css");121 + assert!(css.contains("content:attr(data-label)"), "label rule");122 + assert!(css.contains("@media (max-width:767px)"), "narrow query");123 + let dir = mktemp("labels");124 + git(&dir, &["init", "-b", "main"]);125 + git(&dir, &["config", "user.name", "hu13-test"]);126 + git(&dir, &["config", "user.email", "[email protected]"]);127 + git(&dir, &["config", "commit.gpgsign", "false"]);128 + std::fs::write(dir.join("x.txt"), "x\n").unwrap();129 + git(&dir, &["add", "-A"]);130 + git(&dir, &["commit", "-m", "labeled subject"]);131 + let out = mktemp("out");132 + render(&dir, &out, "main", 100, "test", "<nav>", None).expect("render");133 + let list = std::fs::read_to_string(out.join("commits.html")).expect("list");134 + let tds = list.matches("<td").count();135 + assert!(tds > 0, "has cells");136 + assert_eq!(list.matches("data-label=").count(), tds, "every td labeled");137 + for label in ["sha", "subject", "author", "date"] {138 + assert!(139 + list.contains(&format!("data-label=\"{label}\"")),140 + "label {label}"141 + );142 + }143 + }144 }145 diff --git a/src/forge.rs b/src/forge.rs146 index fe2c0e4..b41ed7a 100644147 --- a/src/forge.rs148 +++ b/src/forge.rs149 @@ -986,7 +986,7 @@ fn render_issues_index(150 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");151 for issue in &data.issues {152 body.push_str(&format!(153 - "<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",154 + "<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",155 n = issue.number,156 id = esc(&issue.id),157 title = esc(&issue.title),158 @@ -1045,7 +1045,7 @@ fn render_pulls_index(159 let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));160 let draft = if pr.draft { " draft" } else { "" };161 body.push_str(&format!(162 - "<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",163 + "<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",164 n = pr.number,165 id = esc(&pr.id),166 title = esc(&pr.title),167 @@ -1143,7 +1143,7 @@ fn render_ci_index(ci: &CiInfo, site_name: &str, nav: &str, description: Option<168 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");169 for s in &run.steps {170 body.push_str(&format!(171 - "<tr><td><code>{}</code></td><td>{}</td><td>{}</td></tr>\n",172 + "<tr><td data-label=\"step\"><code>{}</code></td><td data-label=\"exit\">{}</td><td data-label=\"duration_ms\">{}</td></tr>\n",173 esc(&s.name),174 s.exit,175 s.duration_ms,176 @@ -1690,7 +1690,7 @@ mod tests {177 html.contains("<div class=\"table-scroll\"><table>"),178 "{page}: wrapper"179 );180 - assert!(html.contains("<td class=\"title\">"), "{page}: title cell");181 + assert!(html.contains("<td class=\"title\""), "{page}: title cell");182 assert!(html.contains(&long_title), "{page}: long title kept");183 }184 let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");185 @@ -1750,4 +1750,96 @@ mod tests {186 assert!(pulls.contains("1 closed"), "pulls closed total");187 assert!(pulls.contains(" draft"), "draft stays marked");188 }189 +190 + /// m6/no-scroll (f0rge #39): stacked-card reflow on narrow, desktop untouched.191 + /// Research: from knowledge of Gitea mobile (issue/PR lists collapse to192 + /// title-led cards; file table stacks name + meta) — no live fetch here.193 + #[test]194 + fn stacked_cards_no_scroll() {195 + let css = include_str!("../static/style.css");196 + assert!(css.contains("@media (max-width:767px)"), "narrow query");197 + assert!(css.contains("overflow-x:visible"), "no scroll on narrow");198 + assert!(199 + css.contains("content:attr(data-label)"),200 + "label via ::before"201 + );202 + assert!(css.contains(".table-scroll td::before"), "label rule");203 + assert!(204 + css.contains(".table-scroll td.title::before"),205 + "title leads card"206 + );207 + assert!(208 + css.contains("display:block") && css.contains("display:flex"),209 + "card layout"210 + );211 + assert!(css.contains("width:max-content"), "desktop width kept");212 + assert!(213 + css.contains(".table-scroll th,.table-scroll td") && css.contains("white-space:nowrap"),214 + "desktop nowrap kept"215 + );216 + let repo = mktemp("cards");217 + write_forge(218 + &repo,219 + "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",220 + "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: Card issue\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",221 + );222 + write_forge(223 + &repo,224 + "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",225 + "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: Card pr\nstate: open\nbase: main\nhead: feat\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",226 + );227 + write_forge(228 + &repo,229 + "tasks/cccccccc-cccc-cccc-cccc-cccccccccccc.md",230 + "---\nid: cccccccc-cccc-cccc-cccc-cccccccccccc\ntitle: Card task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nwork\n",231 + );232 + write_forge(&repo, "ci/ci.env", "runner: local\n");233 + std::fs::create_dir_all(repo.join(".forge").join("ci/steps")).unwrap();234 + std::fs::write(235 + repo.join(".forge").join("ci/steps/00_test.sh"),236 + "#!/bin/sh\n",237 + )238 + .unwrap();239 + write_forge(240 + &repo,241 + "ci/runs/2026-09-10T00-00-00Z/summary.md",242 + "---\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",243 + );244 + let out = mktemp("cards-out");245 + render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");246 + for (page, labels) in [247 + (248 + "issues/index.html",249 + vec!["#", "title", "state", "comments", "created"],250 + ),251 + (252 + "pulls/index.html",253 + vec!["#", "title", "state", "head→base", "created"],254 + ),255 + ("ci/index.html", vec!["step", "exit", "duration_ms"]),256 + ] {257 + let html = std::fs::read_to_string(out.join(page)).expect(page);258 + let tds = html.matches("<td").count();259 + assert!(tds > 0, "{page} has cells");260 + assert_eq!(261 + html.matches("data-label=").count(),262 + tds,263 + "{page}: every td labeled"264 + );265 + for label in labels {266 + assert!(267 + html.contains(&format!("data-label=\"{label}\"")),268 + "{page}: label {label}"269 + );270 + }271 + }272 + let tasks = std::fs::read_to_string(out.join("tasks/index.html")).expect("tasks");273 + assert!(!tasks.contains("table-scroll"), "tasks: no scroll wrapper");274 + assert!(tasks.contains("todo (1)"), "tasks group kept");275 + let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");276 + assert!(277 + issues.contains("open") && issues.contains("2026-09-10"),278 + "issues data kept in DOM"279 + );280 + }281 }282 diff --git a/src/releases.rs b/src/releases.rs283 index ad238d9..87493bb 100644284 --- a/src/releases.rs285 +++ b/src/releases.rs286 @@ -391,7 +391,7 @@ pub fn render_loaded(287 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");288 for rel in releases {289 body.push_str(&format!(290 - "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\">{t}</td><td>{c}</td><td>{n}</td></tr>\n",291 + "<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",292 v = esc(&rel.version),293 ve = esc(&rel.version),294 t = esc(rel.display_title()),295 @@ -493,7 +493,7 @@ mod tests {296 index.contains("<div class=\"table-scroll\"><table>"),297 "wrapper"298 );299 - assert!(index.contains("<td class=\"title\">"), "title cell");300 + assert!(index.contains("<td class=\"title\""), "title cell");301 assert!(index.contains(&long_title), "long title kept");302 }303 304 @@ -510,4 +510,46 @@ mod tests {305 std::fs::create_dir_all(&dir).unwrap();306 assert!(load(&dir).is_empty());307 }308 +309 + /// m6/no-scroll (f0rge #39): release rows carry data-labels for cards.310 + #[test]311 + fn release_cells_have_data_labels() {312 + let css = include_str!("../static/style.css");313 + assert!(css.contains("content:attr(data-label)"), "label rule");314 + let rel = Release {315 + version: "v1.2.3".to_string(),316 + title: Some("Labeled".to_string()),317 + created: "2026-09-10T03:00:00Z".to_string(),318 + sha: None,319 + body: "notes\n".to_string(),320 + artifacts: Vec::new(),321 + };322 + let repo = std::env::temp_dir().join(format!(323 + "hu13-rel-labels-{}-{}",324 + std::process::id(),325 + std::time::SystemTime::now()326 + .duration_since(std::time::UNIX_EPOCH)327 + .map(|d| d.as_nanos())328 + .unwrap_or(0)329 + ));330 + std::fs::create_dir_all(&repo).unwrap();331 + let out = repo.join("out");332 + std::fs::create_dir_all(&out).unwrap();333 + render_loaded(&[rel], &repo, &out, "test", "<nav>", None).expect("render");334 + let index =335 + std::fs::read_to_string(out.join("releases").join("index.html")).expect("index");336 + let tds = index.matches("<td").count();337 + assert!(tds > 0, "has cells");338 + assert_eq!(339 + index.matches("data-label=").count(),340 + tds,341 + "every td labeled"342 + );343 + for label in ["version", "title", "created", "artifacts"] {344 + assert!(345 + index.contains(&format!("data-label=\"{label}\"")),346 + "label {label}"347 + );348 + }349 + }350 }351 diff --git a/static/style.css b/static/style.css352 index a09ecf2..2efee6d 100644353 --- a/static/style.css354 +++ b/static/style.css355 @@ -183,3 +183,24 @@ details.clonebox{margin:1rem 0;padding:.25rem 1rem 1rem;background:var(--bg-alt)356 details.clonebox>summary{cursor:pointer;min-height:40px;font-weight:700;color:var(--cyan)}357 details.clonebox>summary:focus-visible{outline:2px solid var(--blue);outline-offset:2px}358 .clone-hint,.clonebox summary .muted{font-weight:400}359 +/* Stacked cards on narrow (m6/no-scroll, f0rge #39): Gitea-style reflow.360 + Desktop (>=768px) keeps .table-scroll horizontal scroll untouched above.361 + Narrow: no scroll (overflow visible), thead clipped for SR only, each362 + tr becomes a bordered card, each td a labeled flex row via data-label.363 + Title cells lead bold without a repeated label; nothing is deleted. */364 +@media (max-width:767px){365 + .table-scroll{overflow-x:visible}366 + .table-scroll table,.file-table table{width:100%;min-width:0}367 + .table-scroll thead{position:absolute;max-width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}368 + .table-scroll tbody,.table-scroll tr{display:block;width:auto}369 + .table-scroll tr{border:1px solid var(--border);background:var(--bg-alt);margin:0 0 .75rem;padding:.25rem 0}370 + .table-scroll th,.table-scroll td,.file-table th:nth-child(3),.file-table td:nth-child(3){white-space:normal}371 + .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}372 + .table-scroll td:last-child{border-bottom:none}373 + .table-scroll td::before{content:attr(data-label);flex:0 0 6rem;color:var(--muted);font-weight:600;font-size:.8rem;overflow-wrap:anywhere}374 + .table-scroll td.title::before{display:none}375 + .table-scroll td.title{font-weight:700;font-size:1.02rem;min-width:0}376 + .table-scroll td.num,.table-scroll td.sha{width:auto}377 + .table-scroll td code,.table-scroll td a{overflow-wrap:anywhere;word-break:break-all;min-width:0}378 + .table-scroll tbody tr:hover td{background:none}379 +}380 diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs381 index d695600..b3aa723 100644382 --- a/tests/forge_readers.rs383 +++ b/tests/forge_readers.rs384 @@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {385 386 // --- issues index: 2 rendered rows (broken one skipped) ---387 let issues = read("issues/index.html");388 - let rows = issues.matches("<tr><td class=\"num\">").count();389 + let rows = issues.matches("<tr><td class=\"num\"").count();390 assert_eq!(rows, 2, "rendered issues match file-count minus broken");391 assert!(!issues.contains("No title here"), "broken issue skipped");392 // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.393 assert!(394 issues.contains(&format!(395 - "<td class=\"num\">1</td><td class=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"396 + "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"397 )),398 "issue #1 -> older uuid"399 );400 assert!(401 issues.contains(&format!(402 - "<td class=\"num\">2</td><td class=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"403 + "<td class=\"num\" data-label=\"#\">2</td><td class=\"title\" data-label=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"404 )),405 "issue #2 -> newer uuid"406 );407 @@ -263,13 +263,13 @@ fn forge_fixture_renders_counts_numbers_and_ci() {408 // (4): pulls — same shape + head/base/draft/ci + patch <pre>.409 let pulls = read("pulls/index.html");410 assert_eq!(411 - pulls.matches("<tr><td class=\"num\">").count(),412 + pulls.matches("<tr><td class=\"num\"").count(),413 1,414 "prs match file-count"415 );416 assert!(417 pulls.contains(&format!(418 - "<td class=\"num\">1</td><td class=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"419 + "<td class=\"num\" data-label=\"#\">1</td><td class=\"title\" data-label=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"420 )),421 "pr #1 -> uuid"422 );423 diff --git a/tests/releases_api.rs b/tests/releases_api.rs424 index 9ea9a71..ea32b2c 100644425 --- a/tests/releases_api.rs426 +++ b/tests/releases_api.rs427 @@ -110,7 +110,7 @@ fn releases_artifacts_api_agents_feed() {428 429 // --- releases: index + both detail pages ---430 let index = read("releases/index.html");431 - assert_eq!(index.matches("<tr><td>").count(), 2, "two release rows");432 + assert_eq!(index.matches("<tr><td").count(), 2, "two release rows");433 assert!(index.contains("v1.0"), "v1.0 listed");434 assert!(index.contains("v2.0"), "v2.0 listed");435 436 @@ -154,15 +154,15 @@ fn releases_artifacts_api_agents_feed() {437 438 // --- api counts == html counts (same struct pass) ---439 let issues_html = read("issues/index.html")440 - .matches("<tr><td class=\"num\">")441 + .matches("<tr><td class=\"num\"")442 .count();443 let pulls_html = read("pulls/index.html")444 - .matches("<tr><td class=\"num\">")445 + .matches("<tr><td class=\"num\"")446 .count();447 let commits_html = read("commits.html")448 - .matches("<tr><td class=\"sha\">")449 + .matches("<tr><td class=\"sha\"")450 .count();451 - let releases_html = read("releases/index.html").matches("<tr><td>").count();452 + let releases_html = read("releases/index.html").matches("<tr><td").count();453 let wiki_html = read("wiki/index.html")454 .matches("<li><a href=\"/wiki/")455 .count();456 diff --git a/tests/responsive.rs b/tests/responsive.rs457 index 37b536e..f0c4b2d 100644458 --- a/tests/responsive.rs459 +++ b/tests/responsive.rs460 @@ -377,23 +377,23 @@ fn forge_list_tables_scroll_and_wrap_long_titles() {461 for page in ["issues/index.html", "pulls/index.html"] {462 let html = read(page);463 assert!(464 - html.contains("<td class=\"title\">"),465 + html.contains("<td class=\"title\""),466 "{page} marks title cells"467 );468 assert!(469 html.contains(&long_title),470 "{page} keeps the full long title"471 );472 - assert!(html.contains("<td class=\"num\">"), "{page} keeps # narrow");473 + assert!(html.contains("<td class=\"num\""), "{page} keeps # narrow");474 }475 let commits = read("commits.html");476 assert!(477 - commits.contains("<td class=\"sha\">"),478 + commits.contains("<td class=\"sha\""),479 "commits marks sha cells"480 );481 let releases = read("releases/index.html");482 assert!(483 - releases.contains("<td class=\"title\">"),484 + releases.contains("<td class=\"title\""),485 "releases marks title cells"486 );487 // Labels from the fixture still list on the issues index.488 -- 489 2.55.0490