.forge/prs/a88ce902-5705-465b-9196-11f427b39a01/patch.diff 13.8 KiB 320 lines Raw
1 From 92b745308da96c1b0bde1608af185550dfe3d023 Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 06:39:28 +01004 Subject: [PATCH] m6/tables: scroll containers on all list tables (115 tests)5 6 ---7  src/commits.rs         |   2 +-8  src/forge.rs           |   4 +-9  src/releases.rs        |   2 +-10  static/style.css       |   3 +11  tests/forge_readers.rs |  16 ++--12  tests/releases_api.rs  |  12 ++-13  tests/responsive.rs    | 164 +++++++++++++++++++++++++++++++++++++++++14  7 files changed, 191 insertions(+), 12 deletions(-)15 16 diff --git a/src/commits.rs b/src/commits.rs17 index a98c017..d258631 10064418 --- a/src/commits.rs19 +++ b/src/commits.rs20 @@ -38,7 +38,7 @@ pub fn render_loaded(21          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");22          for c in log {23              body.push_str(&format!(24 -                "<tr><td><a href=\"/commit/{}.html\"><code>{}</code></a></td><td>{}</td><td>{}</td><td>{}</td></tr>\n",25 +                "<tr><td class=\"sha\"><a href=\"/commit/{}.html\"><code>{}</code></a></td><td class=\"title\">{}</td><td>{}</td><td>{}</td></tr>\n",26                  esc(&c.sha),27                  esc(short_sha(&c.sha)),28                  esc(&c.subject),29 diff --git a/src/forge.rs b/src/forge.rs30 index 868449e..de99cc1 10064431 --- a/src/forge.rs32 +++ b/src/forge.rs33 @@ -940,7 +940,7 @@ fn render_issues_index(data: &ForgeData, site_name: &str, nav: &str) -> Result<S34          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");35          for issue in &data.issues {36              body.push_str(&format!(37 -                "<tr><td>{n}</td><td><a href=\"/issues/{id}.html\">{title}</a></td><td>{state}</td><td>{comments}</td><td>{created}</td></tr>\n",38 +                "<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",39                  n = issue.number,40                  id = esc(&issue.id),41                  title = esc(&issue.title),42 @@ -982,7 +982,7 @@ fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str) -> String {43              let lanes = format!("{}→{}", esc(&pr.head), esc(&pr.base));44              let draft = if pr.draft { " draft" } else { "" };45              body.push_str(&format!(46 -                "<tr><td>{n}</td><td><a href=\"/pulls/{id}.html\">{title}</a>{draft}</td><td>{state}</td><td><code>{lanes}</code></td><td>{created}</td></tr>\n",47 +                "<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",48                  n = pr.number,49                  id = esc(&pr.id),50                  title = esc(&pr.title),51 diff --git a/src/releases.rs b/src/releases.rs52 index e05fe4c..d0de7fd 10064453 --- a/src/releases.rs54 +++ b/src/releases.rs55 @@ -383,7 +383,7 @@ pub fn render_loaded(56          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");57          for rel in releases {58              body.push_str(&format!(59 -                "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td>{t}</td><td>{c}</td><td>{n}</td></tr>\n",60 +                "<tr><td><a href=\"/releases/{v}.html\">{ve}</a></td><td class=\"title\">{t}</td><td>{c}</td><td>{n}</td></tr>\n",61                  v = esc(&rel.version),62                  ve = esc(&rel.version),63                  t = esc(rel.display_title()),64 diff --git a/static/style.css b/static/style.css65 index 2bac0f6..32f05a8 10064466 --- a/static/style.css67 +++ b/static/style.css68 @@ -42,6 +42,9 @@ th,td{text-align:left;padding:.5rem .75rem;border:1px solid #292e42;vertical-ali69  th{background:#1f2335;color:#7dcfff}70  tr:nth-child(even) td{background:#1a1b26}71  td code,th code{word-break:break-all}72 +/* List tables (m6/tables): long titles/shas wrap inside the scroll box; the # column stays narrow. */73 +td.title,td.sha{overflow-wrap:anywhere}74 +td.num{width:1%;white-space:nowrap}75  /* Single column on mobile; side-by-side only when it fits (>=768px). */76  .cols{display:grid;grid-template-columns:minmax(0,1fr);gap:1.5rem;min-width:0}77  .cols>section,.cols>div{min-width:0}78 diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs79 index f512a39..fda2b8b 10064480 --- a/tests/forge_readers.rs81 +++ b/tests/forge_readers.rs82 @@ -224,19 +224,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() {83  84      // --- issues index: 2 rendered rows (broken one skipped) ---85      let issues = read("issues/index.html");86 -    let rows = issues.matches("<tr><td>").count();87 +    let rows = issues.matches("<tr><td class=\"num\">").count();88      assert_eq!(rows, 2, "rendered issues match file-count minus broken");89      assert!(!issues.contains("No title here"), "broken issue skipped");90      // 5-number spot check (1): #1 links the older uuid, #2 the newer uuid.91      assert!(92          issues.contains(&format!(93 -            "<td>1</td><td><a href=\"/issues/{OLDER_UUID}.html\">"94 +            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/issues/{OLDER_UUID}.html\">"95          )),96          "issue #1 -> older uuid"97      );98      assert!(99          issues.contains(&format!(100 -            "<td>2</td><td><a href=\"/issues/{NEWER_UUID}.html\">"101 +            "<td class=\"num\">2</td><td class=\"title\"><a href=\"/issues/{NEWER_UUID}.html\">"102          )),103          "issue #2 -> newer uuid"104      );105 @@ -262,9 +262,15 @@ fn forge_fixture_renders_counts_numbers_and_ci() {106  107      // (4): pulls — same shape + head/base/draft/ci + patch <pre>.108      let pulls = read("pulls/index.html");109 -    assert_eq!(pulls.matches("<tr><td>").count(), 1, "prs match file-count");110 +    assert_eq!(111 +        pulls.matches("<tr><td class=\"num\">").count(),112 +        1,113 +        "prs match file-count"114 +    );115      assert!(116 -        pulls.contains(&format!("<td>1</td><td><a href=\"/pulls/{PR_UUID}.html\">")),117 +        pulls.contains(&format!(118 +            "<td class=\"num\">1</td><td class=\"title\"><a href=\"/pulls/{PR_UUID}.html\">"119 +        )),120          "pr #1 -> uuid"121      );122      let pr = read(&format!("pulls/{PR_UUID}.html"));123 diff --git a/tests/releases_api.rs b/tests/releases_api.rs124 index 57345e3..9ea9a71 100644125 --- a/tests/releases_api.rs126 +++ b/tests/releases_api.rs127 @@ -153,9 +153,15 @@ fn releases_artifacts_api_agents_feed() {128      );129  130      // --- api counts == html counts (same struct pass) ---131 -    let issues_html = read("issues/index.html").matches("<tr><td>").count();132 -    let pulls_html = read("pulls/index.html").matches("<tr><td>").count();133 -    let commits_html = read("commits.html").matches("<tr><td>").count();134 +    let issues_html = read("issues/index.html")135 +        .matches("<tr><td class=\"num\">")136 +        .count();137 +    let pulls_html = read("pulls/index.html")138 +        .matches("<tr><td class=\"num\">")139 +        .count();140 +    let commits_html = read("commits.html")141 +        .matches("<tr><td class=\"sha\">")142 +        .count();143      let releases_html = read("releases/index.html").matches("<tr><td>").count();144      let wiki_html = read("wiki/index.html")145          .matches("<li><a href=\"/wiki/")146 diff --git a/tests/responsive.rs b/tests/responsive.rs147 index 1b21cf9..37b536e 100644148 --- a/tests/responsive.rs149 +++ b/tests/responsive.rs150 @@ -285,3 +285,167 @@ fn zero_js_and_reader_friendly() {151          }152      }153  }154 +155 +fn forge_write(repo: &Path, rel: &str, content: &str) {156 +    let dest = repo.join(".forge").join(rel);157 +    std::fs::create_dir_all(dest.parent().expect("parent")).unwrap();158 +    std::fs::write(&dest, content).unwrap();159 +}160 +161 +/// Long-title forge fixture (m6, f0rge issue #17): one 200-char unbroken162 +/// title on an issue (+ labels) and on a PR, plus one task, one CI run163 +/// with steps, and one release — so every list page renders non-empty.164 +/// Returns `(dist_dir, long_title)`.165 +fn build_long_title_fixture() -> (PathBuf, String) {166 +    let repo = fixture_repo();167 +    let long_title = "L".repeat(200);168 +    forge_write(169 +        &repo,170 +        "issues/aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa/issue.md",171 +        &format!(172 +            "---\nid: aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaaaa\ntitle: {long_title}\nstate: open\nlabels:\n- kind:scroll-test\nassignees: []\ncreated: 2026-09-10T03:00:00Z\n---\nLong-title body.\n"173 +        ),174 +    );175 +    forge_write(176 +        &repo,177 +        "prs/bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb/pr.md",178 +        &format!(179 +            "---\nid: bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbbbb\ntitle: {long_title}\nstate: open\nbase: main\nhead: long-title-branch\ncreated: 2026-09-10T03:30:00Z\n---\nLong-title PR body.\n"180 +        ),181 +    );182 +    forge_write(183 +        &repo,184 +        "tasks/cccccccc-cccc-4ccc-cccc-cccccccccccc.md",185 +        "---\nid: cccccccc-cccc-4ccc-cccc-cccccccccccc\ntitle: Scroll task\nstatus: todo\ncreated: 2026-09-10T03:00:00Z\n---\nTask body.\n",186 +    );187 +    forge_write(&repo, "labels.md", "- name: kind:scroll-test\n");188 +    forge_write(&repo, "ci/ci.env", "runner: local\n");189 +    forge_write(&repo, "ci/steps/00_wrap.sh", "#!/bin/sh\nexit 0\n");190 +    forge_write(191 +        &repo,192 +        "ci/runs/2026-09-10T00-00-00Z/summary.md",193 +        "---\nrun: 2026-09-10T00-00-00Z\nverdict: pass\ncreated: 2026-09-10T00:00:00Z\nsteps:\n  - name: 00_wrap.sh\n    exit: 0\n    duration_ms: 3\n---\nwrap log\n",194 +    );195 +    forge_write(196 +        &repo,197 +        "releases/v9.9.md",198 +        "---\nversion: v9.9\ntitle: Scroll release\ncreated: 2026-09-10T02:00:00Z\n---\nRelease notes.\n",199 +    );200 +    git(&repo, &["add", "-A"]);201 +    git(&repo, &["commit", "-m", "long-title forge fixture"]);202 +    let out = fresh("tables");203 +    let st = Command::new(bin())204 +        .arg("build")205 +        .arg(&repo)206 +        .arg("-o")207 +        .arg(&out)208 +        .status()209 +        .expect("spawn hu13 build");210 +    assert!(st.success(), "long-title fixture build exits 0");211 +    (out, long_title)212 +}213 +214 +#[test]215 +fn forge_list_tables_scroll_and_wrap_long_titles() {216 +    let (out, long_title) = build_long_title_fixture();217 +    let read = |rel: &str| std::fs::read_to_string(out.join(rel)).expect(rel);218 +    // Every table-bearing list page wraps its table in .table-scroll and219 +    // leaves no bare <table> outside the scroll container. The wrapper is220 +    // the 360px story: the table scrolls inside instead of forcing the page.221 +    for page in [222 +        "issues/index.html",223 +        "pulls/index.html",224 +        "commits.html",225 +        "releases/index.html",226 +        "ci/index.html",227 +    ] {228 +        let html = read(page);229 +        assert!(html.contains("table-scroll"), "{page} wraps its table");230 +        assert!(html.contains("<table"), "{page} keeps <table>");231 +        let stripped = html.replace("<div class=\"table-scroll\"><table>", "");232 +        assert!(233 +            !stripped.contains("<table"),234 +            "{page} has no bare <table> outside .table-scroll"235 +        );236 +    }237 +    // Tasks list renders grouped <ul>s, never a table.238 +    let tasks = read("tasks/index.html");239 +    assert!(!tasks.contains("<table"), "tasks page has no <table>");240 +    // 200-char titles render in full (wrapped, not cut) inside title cells:241 +    // the cell class is the no-page-widen hook (overflow-wrap:anywhere),242 +    // and the # column keeps its narrow marker class.243 +    for page in ["issues/index.html", "pulls/index.html"] {244 +        let html = read(page);245 +        assert!(246 +            html.contains("<td class=\"title\">"),247 +            "{page} marks title cells"248 +        );249 +        assert!(250 +            html.contains(&long_title),251 +            "{page} keeps the full long title"252 +        );253 +        assert!(html.contains("<td class=\"num\">"), "{page} keeps # narrow");254 +    }255 +    let commits = read("commits.html");256 +    assert!(257 +        commits.contains("<td class=\"sha\">"),258 +        "commits marks sha cells"259 +    );260 +    let releases = read("releases/index.html");261 +    assert!(262 +        releases.contains("<td class=\"title\">"),263 +        "releases marks title cells"264 +    );265 +    // Labels from the fixture still list on the issues index.266 +    assert!(267 +        read("issues/index.html").contains("kind:scroll-test"),268 +        "labels listed"269 +    );270 +    // CSS side: title/sha cells wrap anywhere, # column stays narrow.271 +    let css = css();272 +    for needle in [273 +        "td.title",274 +        "td.sha",275 +        "td.num",276 +        "overflow-wrap:anywhere",277 +        "white-space:nowrap",278 +    ] {279 +        assert!(css.contains(needle), "CSS mentions {needle}");280 +    }281 +}282 +283 +#[test]284 +fn zero_bare_tables_in_sources() {285 +    // Audit EVERY template (m6, f0rge issue #17): no `<table>` may open286 +    // outside a `.table-scroll` container. All tables are emitted from287 +    // src/*.rs render fns (markdown never emits tables), so grep the288 +    // sources: each line opening a table must carry the scroll wrapper.289 +    let mut checked = 0;290 +    let mut stack = vec![manifest().join("src")];291 +    while let Some(d) = stack.pop() {292 +        for e in std::fs::read_dir(&d).expect("read src") {293 +            let e = e.expect("entry");294 +            let p = e.path();295 +            if p.is_dir() {296 +                stack.push(p);297 +                continue;298 +            }299 +            if p.extension().is_none_or(|x| x != "rs") {300 +                continue;301 +            }302 +            let text = std::fs::read_to_string(&p).expect("read source");303 +            for (i, line) in text.lines().enumerate() {304 +                if line.contains("<table") {305 +                    checked += 1;306 +                    assert!(307 +                        line.contains("table-scroll"),308 +                        "{}:{} opens a bare <table>",309 +                        p.display(),310 +                        i + 1311 +                    );312 +                }313 +            }314 +        }315 +    }316 +    assert!(checked >= 5, "audit saw all list tables, got {checked}");317 +}318 -- 319 2.55.0320