From 89d1730312f68be76ac1c34295dc867c3ba0a33f Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 10:15:47 +0100 Subject: [PATCH] m6/finish2: spacing, 2x logo, footer right, clone summary meta --- src/build.rs | 123 ++++++++++++++++++++++++++++++----------- src/commits.rs | 2 +- src/forge.rs | 4 +- src/inbox.rs | 2 +- src/layout.rs | 48 +++++++++++++++- src/releases.rs | 2 +- src/wiki.rs | 2 +- static/style.css | 15 +++-- tests/code_space.rs | 57 ++++++++++++------- tests/forge_readers.rs | 15 ++++- tests/responsive.rs | 6 +- 11 files changed, 207 insertions(+), 69 deletions(-) diff --git a/src/build.rs b/src/build.rs index f1c0795..68698e3 100644 --- a/src/build.rs +++ b/src/build.rs @@ -639,7 +639,9 @@ pub fn file_table_html( /// bundle-first as before (`git clone ` first when a clone URL is /// configured, else local bundle) plus the bundle/tarball links with /// sizes and shas. The `` always shows `Clone & download` -/// plus a branch@sha hint. +/// plus a branch@sha hint. The summary also carries the CI verdict badge +/// (m6 finish, f0rge #53): `Clone & download · branch @ sha · CI badge` +/// so the standalone repo-meta branch/CI line is gone (no duplication). #[must_use] #[allow(clippy::too_many_arguments)] pub fn clone_block( @@ -647,6 +649,7 @@ pub fn clone_block( public_origin: Option<&str>, branch: &str, short_sha: &str, + ci_verdict: &str, bundle_size: u64, bundle_sha: Option<&str>, tarball_size: u64, @@ -674,11 +677,13 @@ pub fn clone_block( human_size(tarball_size) ), }; + let badge = crate::forge::verdict_badge(ci_verdict); let mut out = format!( "
\n\ - Clone & download {} @ {}\n
\n", + Clone & download \u{b7} {} @ {} \u{b7} CI {}\n
\n", esc(branch), - esc(short_sha) + esc(short_sha), + badge ); if let Some(origin) = public_origin.map(str::trim).filter(|s| !s.is_empty()) { let base = origin.trim_end_matches('/'); @@ -1071,28 +1076,21 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result

{}

\n", esc(&name)); if let Some(d) = &description { body.push_str(&format!("

{}

\n", esc(d))); } - body.push_str(&format!( - "

Branch {} @ {}

\n", - esc(&branch), - esc(&short_sha) - )); - body.push_str(&format!( - "

CI: {}

\n", - forge::verdict_badge(forge_data.ci.latest_verdict()), - )); body.push_str(&clone_block( clone_url.as_deref(), public_origin.as_deref(), &branch, &short_sha, + forge_data.ci.latest_verdict(), bundle_size, file_hash(&snap.bundle).as_deref(), tarball_size, @@ -1284,18 +1282,25 @@ mod tests { "raw HTML escaped" ); assert!(index.contains("<script>"), "escaped HTML present"); - // Header: branch + short sha, live open f0rge counts, CI verdict badge. + // Header (m6 finish, f0rge #53): branch/sha/CI live ONLY in the + // clone summary — no standalone repo-meta / CI dupe line. assert!(index.contains("main"), "branch shown"); assert!(!index.contains("TODO"), "no TODO markers left"); assert!(index.contains("issues (0)"), "nav open issues 0"); assert!(index.contains("pulls (0)"), "nav open pulls 0"); - assert!(index.contains("repo-meta"), "branch/sha meta kept"); - assert!(index.contains("CI:"), "CI verdict shown"); - assert!(index.contains("badge"), "CI verdict badge"); + assert!(!index.contains("repo-meta"), "no dupe repo-meta line"); + assert!(!index.contains("

CI:"), "no dupe CI line"); + assert!(index.contains("badge"), "CI verdict badge in summary"); + let summ = index.find("

Clone").expect("summary"); + let send = index[summ..].find("").expect("summary end") + summ; + let summary = &index[summ..send]; + assert!(summary.contains("main"), "branch in summary"); + assert!(summary.contains("CI"), "CI in summary"); + assert!(summary.contains("badge"), "badge in summary"); // f0rge #36: code tab carries no redundant counts line. let head_start = index.find("
").expect("repo-head"); - let head_end = index[head_start..].find("
").expect("repo-head end") + head_start; - let head = &index[head_start..head_end]; + let details_end = index[head_start..].find("
").expect("details end") + head_start; + let head = &index[head_start..details_end]; for needle in ["issues (", "pulls (", "tasks (", "releases ("] { assert!(!head.contains(needle), "code tab drops {needle}"); } @@ -1362,11 +1367,15 @@ mod tests { let index = std::fs::read_to_string(out.join("index.html")).expect("index"); assert!(index.contains("issues (1)"), "nav open issues"); assert!(index.contains("pulls (2)"), "nav open pulls (draft incl)"); - assert!(index.contains("repo-meta"), "branch/sha meta kept"); - assert!(index.contains("CI:"), "CI meta kept"); + assert!( + !index.contains("repo-meta"), + "no dupe repo-meta (f0rge #53)" + ); + assert!(!index.contains("

CI:"), "no dupe CI line (f0rge #53)"); + assert!(index.contains("badge"), "CI badge in summary"); let head_start = index.find("

").expect("repo-head"); - let head_end = index[head_start..].find("
").expect("repo-head end") + head_start; - let head = &index[head_start..head_end]; + let details_end = index[head_start..].find("").expect("details end") + head_start; + let head = &index[head_start..details_end]; for needle in ["issues (", "pulls (", "tasks (", "releases ("] { assert!(!head.contains(needle), "code tab drops {needle}"); } @@ -2041,13 +2050,15 @@ mod tests { let out = mktemp(&format!("out-ci-{verdict}")); run_build(&repo, &out, &opts()).expect("build"); let index = std::fs::read_to_string(out.join("index.html")).expect("index"); - let head_start = index.find("
").expect("repo-head"); - let head_end = index[head_start..].find("
").expect("repo-head end") + head_start; - let head = &index[head_start..head_end]; + let summ = index.find("Clone").expect("summary"); + let send = index[summ..].find("").expect("summary end") + summ; + let summary = &index[summ..send]; assert!( - head.contains(&format!("badge {verdict}")), - "code head carries badge {verdict}" + summary.contains(&format!("badge {verdict}")), + "clone summary carries badge {verdict}" ); + assert!(!index.contains("repo-meta"), "no dupe meta"); + assert!(!index.contains("

CI:"), "no dupe CI line"); } } @@ -2249,4 +2260,54 @@ mod tests { "ignored still in raw" ); } + + #[test] + fn finish_page_heads_and_clone_summary() { + // m6 finish (f0rge #50 + #53): one head pattern everywhere, branch+CI + // only in the clone summary, no dupe line. + let (_repo, out) = build_fixture(); + let read = |rel: &str| std::fs::read_to_string(out.join(rel)).expect(rel); + let index = read("index.html"); + // Code tab: repo-head with h1, summary carries branch @ sha + CI. + let head_pos = index.find("

").expect("repo-head"); + assert!(index[head_pos..].contains("

"), "code h1 in repo-head"); + let s0 = index.find("Clone").expect("summary"); + let s1 = index[s0..].find("").expect("summary end") + s0; + let summary = &index[s0..s1]; + assert!(summary.contains("Clone & download"), "summary label"); + assert!(summary.contains("main"), "branch in summary"); + assert!(summary.contains("CI"), "CI in summary"); + assert!(summary.contains("badge"), "CI badge in summary"); + assert!(!index.contains("repo-meta"), "no dupe repo-meta"); + assert!(!index.contains("

CI:"), "no dupe CI line"); + // Branch appears in summary; outside details it is not repeated as + // a meta line (description/counts carry no branch text here). + let details_pos = index.find("

head_pos && s0 >= details_pos, + "summary inside code head" + ); + // Every index/Form page opens with the same page-head h1 row. + for (rel, h1) in [ + ("issues/index.html", "

Issues

"), + ("pulls/index.html", "

Pulls

"), + ("commits.html", "

Commits

"), + ("tasks/index.html", "

Tasks

"), + ("ci/index.html", "

CI

"), + ("releases/index.html", "

Releases

"), + ("wiki/index.html", "

Wiki

"), + ("new-issue.html", "

Suggest an issue

"), + ("new-pr.html", "

Suggest a change

"), + ] { + let html = read(rel); + let hpos = html.find("
").expect(rel); + let h1pos = html.find(h1).unwrap_or_else(|| panic!("{rel} missing h1")); + let close = html[hpos..].find("
").expect("close") + hpos; + assert!(h1pos > hpos && h1pos < close, "{rel}: h1 inside page-head"); + } + // Agents guide renders its markdown h1; spacing comes from the + // shared `main>h1:first-child` gap-var rule (CSS assert in layout). + let agents = read("agents.html"); + assert!(agents.contains("

"), "agents h1"); + } } diff --git a/src/commits.rs b/src/commits.rs index e3aa5de..67872bd 100644 --- a/src/commits.rs +++ b/src/commits.rs @@ -36,7 +36,7 @@ pub fn render_loaded( snap_sha: &str, description: Option<&str>, ) -> Result { - let mut body = String::from("

Commits

\n"); + let mut body = String::from("

Commits

\n"); if log.is_empty() { body.push_str("

No commits yet.

\n"); } else { diff --git a/src/forge.rs b/src/forge.rs index 01107e8..6044b16 100644 --- a/src/forge.rs +++ b/src/forge.rs @@ -1161,7 +1161,7 @@ fn render_tasks_index( short_sha: &str, description: Option<&str>, ) -> String { - let mut body = String::from("

Tasks

\n"); + let mut body = String::from("

Tasks

\n"); if tasks.is_empty() { body.push_str("

No tasks yet.

\n"); } else { @@ -1213,7 +1213,7 @@ fn render_ci_index( short_sha: &str, description: Option<&str>, ) -> String { - let mut body = String::from("

CI

\n"); + let mut body = String::from("

CI

\n"); body.push_str(&meta_row("runner:", &esc(&ci.runner))); if ci.steps.is_empty() { body.push_str("

No CI steps configured.

\n"); diff --git a/src/inbox.rs b/src/inbox.rs index ecceb1d..a6cf081 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -619,7 +619,7 @@ fn form_page( description: Option<&str>, ) -> String { let body = format!( - "

{heading}

\n

{intro}

\n\ + "

{heading}

\n

{intro}

\n\

What happens: your text goes to a private inbox only the maintainer reads — \ never published on this site. No account needed. The server answers a one-time \ ack id and shows nothing back.

\n\ diff --git a/src/layout.rs b/src/layout.rs index 096a227..70482a2 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -293,7 +293,7 @@ fn page_site_inner( \n\ \n{head}\n\ \n\ -
\"hu13•
\n\ +
\"hu13•
\n\
\n{body}\n
\n\ {footer}\n\ \n\ @@ -583,6 +583,52 @@ mod tests { assert!(!provenance_footer(Some("main"), Some("")).contains('@')); } + #[test] + fn finish_gap_vars_logo_footer() { + // m6 finish (f0rge #50 + #51 + #52): unified head gaps via vars, + // 48px logo, right footer — all in one CSS + header assert. + let css = include_str!("../static/style.css"); + // #50: single vars, mobile tighter (.75/1rem) via :root, desktop + // looser (1.25/1.5rem) via the existing min-width breakpoint. + assert!(css.contains("--gap-head"), "gap-head var"); + assert!(css.contains("--gap-content"), "gap-content var"); + assert!(css.contains("--gap-head:.75rem"), "mobile head gap"); + assert!(css.contains("--gap-content:1rem"), "mobile content gap"); + assert!(css.contains("--gap-head:1.25rem"), "desktop head gap"); + assert!(css.contains("--gap-content:1.5rem"), "desktop content gap"); + assert!( + css.contains(".page-head") && css.contains("margin-top:var(--gap-head)"), + "page-head uses gap-head" + ); + assert!( + css.contains(".page-head") && css.contains("margin-bottom:var(--gap-content)"), + "page-head uses gap-content" + ); + assert!( + css.contains(".repo-head") && css.contains("margin-top:var(--gap-head)"), + "repo-head uses gap-head" + ); + assert!( + css.contains("main>h1:first-child") && css.contains("margin-top:var(--gap-head)"), + "bare h1 shares nav->head gap" + ); + // #51: 48px logo (was 24px / 1.5rem), vector kept, header centers. + assert!(css.contains(".site-logo"), "logo rule"); + assert!(css.contains("width:3rem"), "logo 48px width"); + assert!(css.contains("height:3rem"), "logo 48px height"); + assert!(!css.contains("width:1.5rem"), "old 24px gone"); + let html = page("t", "", "

b

"); + assert!( + html.contains("width=\"48\" height=\"48\""), + "header img 48px attrs" + ); + // #52: footer provenance sits right, both themes (pure CSS var block). + assert!( + css.contains(".footer-provenance") && css.contains("text-align:right"), + "footer right-aligned" + ); + } + #[test] fn provenance_footer_names_snapshot_and_escapes() { // m6 footer-line (f0rge #41): build pages name branch @ sha; diff --git a/src/releases.rs b/src/releases.rs index cceb905..03ca8e5 100644 --- a/src/releases.rs +++ b/src/releases.rs @@ -391,7 +391,7 @@ pub fn render_loaded( description: Option<&str>, ) -> Result<(), String> { let root = repo.join(".forge").join("releases"); - let mut body = String::from("

Releases

\n"); + let mut body = String::from("

Releases

\n"); if releases.is_empty() { body.push_str("

No releases yet.

\n"); } else { diff --git a/src/wiki.rs b/src/wiki.rs index 746c229..8e332d5 100644 --- a/src/wiki.rs +++ b/src/wiki.rs @@ -275,7 +275,7 @@ pub fn render( let wiki_dir = out.join("wiki"); std::fs::create_dir_all(&wiki_dir).map_err(|e| format!("hu13: create wiki dir: {e}"))?; - let mut body = String::from("

Wiki

\n"); + let mut body = String::from("

Wiki

\n"); if pages.is_empty() { body.push_str("

No wiki pages yet.

\n"); } else { diff --git a/static/style.css b/static/style.css index a305df3..a75e481 100644 --- a/static/style.css +++ b/static/style.css @@ -16,6 +16,8 @@ --border:#292e42; --visited:#9aa5ce; --btn-fg:#16161e; + --gap-head:.75rem; + --gap-content:1rem; --diff-add:#9ece6a; --diff-add-bg:#233328; --diff-del:#f7768e; @@ -68,7 +70,7 @@ main{padding:1.25rem 0 3rem;min-width:0} CSS margin-top (never
spacers); the base stays tight for 360px and wider screens get a touch more. Title is explicit 2rem so it reads as the page title on every viewport. */ -.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:.75rem} +.repo-head{display:flex;flex-direction:column;gap:.25rem;min-width:0;margin-top:var(--gap-head);margin-bottom:var(--gap-content)} .repo-head h1{margin:0 0 .25rem;overflow-wrap:anywhere;font-size:2rem;line-height:1.2} .clone,.repo-meta{margin:.25rem 0;min-width:0} .clone code,.repo-meta code{overflow-wrap:anywhere;word-break:break-all} @@ -123,7 +125,7 @@ footer.site-footer{border-top:1px solid var(--border)} footer.site-footer .wrap{padding:1rem} /* Provenance line (m6/footer-line, f0rge #41): the single muted line inside the existing footer block — small, no extra whitespace. */ -footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem} +footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem;text-align:right} /* Navbar one-bar (m6/navbar, f0rge #18): logo hu13 • scrollable nav • repo. Own classes only (.header-bar/.brand-lockup/.site-logo/.hu13-word/.sep/ .nav-scroll/.nav-scroll-row); earlier rules above are untouched. The nav @@ -132,7 +134,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem} .site-header .wrap.header-bar{display:flex;flex-wrap:nowrap;align-items:center;gap:.5rem .75rem} .brand-lockup{display:inline-flex;align-items:center;gap:.5rem;flex:none;color:var(--fg);text-decoration:none;min-height:40px} .brand-lockup:visited{color:var(--fg)} -.site-logo{display:block;width:1.5rem;height:1.5rem;flex:none} +.site-logo{display:block;width:3rem;height:3rem;flex:none} .hu13-word{font-weight:700;color:var(--blue)} .sep{flex:none;color:var(--muted)} .site-header .site-nav.nav-scroll{flex:1 1 auto;min-width:0;background:none;border-bottom:none;overflow-x:auto;scroll-snap-type:none;-webkit-overflow-scrolling:touch} @@ -144,7 +146,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem} @media (min-width:768px){ .cols{grid-template-columns:minmax(0,1fr) minmax(0,1.25fr)} .site-nav .wrap{gap:.25rem 1.25rem} - .repo-head{margin-top:1.25rem} + :root{--gap-head:1.25rem;--gap-content:1.5rem} main{padding:1.5rem 0 3rem} } /* Code tab two columns (m6/code-cols, f0rge #45): files + README share @@ -160,7 +162,7 @@ footer.site-footer .footer-provenance{margin:.5rem 0;font-size:.85rem} .cols-code{grid-template-columns:minmax(0,7fr) minmax(0,5fr)} } /* Code conventions (m6, f0rge #20): breadcrumbs, file meta, line anchors, clone block. */ -.crumbs{margin:.5rem 0;font-size:.95rem} +.crumbs{margin:var(--gap-head) 0 .5rem;font-size:.95rem} .crumbs a{color:var(--cyan);text-decoration:none} .crumbs a:hover,.crumbs a:focus{text-decoration:underline} .file-meta{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem 1rem;margin:.5rem 0;padding:.5rem .75rem;background:var(--bg-alt);border:1px solid var(--border)} @@ -195,8 +197,9 @@ a.ln:hover,a.ln:focus{text-decoration:underline} .table-scroll td.trunc-title,.trunc-title{max-width:60ch} /* Page titles with inline buttons (m6/table-scroll, f0rge #31): h1 + button share one flex row; wrap allowed on 360px but stays adjacent. */ -.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem} +.page-head{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1rem;margin-top:var(--gap-head);margin-bottom:var(--gap-content)} .page-head h1{margin:0} +main>h1:first-child{margin-top:var(--gap-head);margin-bottom:var(--gap-content)} .page-head .btn-new{flex:none} /* Code-gitea (m6, f0rge #33 + #34): file TABLE, language bar, clone details. */ diff --git a/tests/code_space.rs b/tests/code_space.rs index 7d73a3a..9e01e8c 100644 --- a/tests/code_space.rs +++ b/tests/code_space.rs @@ -93,22 +93,26 @@ fn parse_rem(value: &str) -> Option { #[test] fn repo_head_spacing_is_css_margin_not_br_tags() { // Structure assert: the code tab keeps its stacked repo-head - // (name + meta + clone block) with air from CSS margin — no `
` - // spacers between the navbar and the repo-head. + // (name + clone summary with branch/CI) with air from CSS margin — + // no `
` spacers between the navbar and the repo-head. + // (m6 finish, f0rge #53: branch/sha/CI live ONLY in the clone summary.) let out = build_fixture(); let index = std::fs::read_to_string(out.join("index.html")).expect("index"); let head_start = index.find("
").expect("repo-head"); - let head_end = index[head_start..].find("
").expect("repo-head end") + head_start; - let head = &index[head_start..head_end]; + let details_end = index[head_start..].find("

").expect("details end") + head_start; + let head = &index[head_start..details_end]; assert!(head.contains("

"), "repo-head keeps its title"); assert!( !head.contains(" tags" ); - assert!( - head.contains("repo-meta"), - "branch/sha meta line kept inside repo-head" - ); + assert!(!index.contains("repo-meta"), "no dupe repo-meta line"); + assert!(!index.contains("

CI:"), "no dupe CI line"); + let s0 = index.find("

Clone").expect("summary"); + let s1 = index[s0..].find("").expect("summary end") + s0; + let summary = &index[s0..s1]; + assert!(summary.contains("main"), "branch in summary"); + assert!(summary.contains("CI"), "CI in summary"); // The header bar ends () before
carries the repo-head: // the break between nav and repo-head is a block boundary + margin. let header_end = index.find("").expect("header close"); @@ -122,13 +126,18 @@ fn repo_head_spacing_is_css_margin_not_br_tags() { "no spacer tags between navbar and repo-head" ); - // CSS assert: `.repo-head` carries the top margin. + // CSS assert: `.repo-head` carries the top margin via --gap-head. let css = css(); let base = css.split("@media (min-width").next().expect("base css"); let margin = rule_value(base, ".repo-head", "margin-top").expect("repo-head margin-top"); assert!( - parse_rem(&margin).is_some_and(|v| v > 0.0), - "repo-head margin-top is positive breathing room, got {margin:?}" + margin.contains("var(--gap-head)"), + "repo-head margin-top uses --gap-head, got {margin:?}" + ); + let gap = rule_value(base, ":root{", "--gap-head").expect("gap-head var"); + assert!( + parse_rem(&gap).is_some_and(|v| v > 0.0), + "gap-head is positive breathing room, got {gap:?}" ); } @@ -154,15 +163,17 @@ fn repo_head_title_reads_as_page_title() { #[test] fn mobile_stays_tight_wider_screens_get_more() { // 360px must not drown in whitespace: the base (pre-media-query) - // margin stays modest; >=768px may open up a touch more. + // --gap-head stays modest; >=768px may open up a touch more. + // (m6 finish, f0rge #50: gaps ride on --gap-head/--gap-content vars.) let css = css(); + assert!(css.contains("--gap-head"), "gap-head var"); + assert!(css.contains("--gap-content"), "gap-content var"); let base = css.split("@media (min-width").next().expect("base css"); - let base_margin = - rule_value(base, ".repo-head", "margin-top").expect("base repo-head margin-top"); - let base_rem = parse_rem(&base_margin).expect("base margin in rem"); + let base_gap = rule_value(base, ":root{", "--gap-head").expect("base gap-head"); + let base_rem = parse_rem(&base_gap).expect("base gap in rem"); assert!( base_rem <= 1.0, - "mobile margin-top stays tight (<=1rem), got {base_margin:?}" + "mobile gap-head stays tight (<=1rem), got {base_gap:?}" ); let desktop = css .split("@media (min-width:768px)") @@ -170,15 +181,19 @@ fn mobile_stays_tight_wider_screens_get_more() { .expect("768px breakpoint"); // Only the current breakpoint block: cut before the next @media. let block = desktop.split("@media").next().expect("768 block"); - let wide_margin = - rule_value(block, ".repo-head", "margin-top").expect("wide repo-head margin-top"); - let wide_rem = parse_rem(&wide_margin).expect("wide margin in rem"); + let wide_gap = rule_value(block, ":root{", "--gap-head").expect("wide gap-head"); + let wide_rem = parse_rem(&wide_gap).expect("wide gap in rem"); assert!( wide_rem >= base_rem, - "wider screens keep at least base air ({wide_margin:?} vs {base_margin:?})" + "wider screens keep at least base air ({wide_gap:?} vs {base_gap:?})" ); assert!( wide_rem <= 2.0, - "even wide stays modest (<=2rem), got {wide_margin:?}" + "even wide stays modest (<=2rem), got {wide_gap:?}" + ); + // Heads share the var (single pattern, not per-page px). + assert!( + base.contains("margin-top:var(--gap-head)"), + "heads use gap-head var" ); } diff --git a/tests/forge_readers.rs b/tests/forge_readers.rs index b43bfcf..27762ef 100644 --- a/tests/forge_readers.rs +++ b/tests/forge_readers.rs @@ -316,10 +316,19 @@ fn forge_fixture_renders_counts_numbers_and_ci() { let index = read("index.html"); assert!(index.contains("issues (1)"), "nav open issue count"); assert!(index.contains("pulls (1)"), "nav open pull count"); - assert!(index.contains("repo-meta"), "branch/sha meta kept"); + assert!( + !index.contains("repo-meta"), + "no dupe repo-meta (f0rge #53)" + ); + assert!(!index.contains("

CI:"), "no dupe CI line (f0rge #53)"); + let summ = index.find("

Clone").expect("summary"); + let send = index[summ..].find("").expect("summary end") + summ; + let summary = &index[summ..send]; + assert!(summary.contains("CI"), "CI in summary"); + assert!(summary.contains("badge fail"), "fail badge in summary"); let head_start = index.find("
").expect("repo-head"); - let head_end = index[head_start..].find("
").expect("repo-head end") + head_start; - let head = &index[head_start..head_end]; + let details_end = index[head_start..].find("").expect("details end") + head_start; + let head = &index[head_start..details_end]; for needle in ["issues (", "pulls (", "tasks (", "releases ("] { assert!(!head.contains(needle), "code tab drops {needle}"); } diff --git a/tests/responsive.rs b/tests/responsive.rs index 85b664e..9879f70 100644 --- a/tests/responsive.rs +++ b/tests/responsive.rs @@ -233,9 +233,13 @@ fn touch_targets_and_stacking_header() { } let out = build_fixture(); let index = std::fs::read_to_string(out.join("index.html")).expect("index"); - for needle in ["repo-head", "cols", "repo-meta"] { + for needle in ["repo-head", "cols", "clonebox"] { assert!(index.contains(needle), "index.html has {needle}"); } + assert!( + !index.contains("repo-meta"), + "no dupe repo-meta (f0rge #53)" + ); } #[test] -- 2.55.0