diff --git a/examples/diff-showcase-report.html b/examples/diff-showcase-report.html index ffc0e9d9..22d0ad00 100644 --- a/examples/diff-showcase-report.html +++ b/examples/diff-showcase-report.html @@ -1730,7 +1730,19 @@ surface (NOT the dark .col-tooltip shell — it reuses the code-card chrome so the reconstruction reads like the drawers). */ .fmt-tooltip { - position: fixed; z-index: 90; max-width: min(34rem, 92vw); max-height: 22rem; overflow: auto; + position: fixed; z-index: 90; max-width: min(34rem, 92vw); + /* cute-dbt#246 — never a reachability fold: the bubble is + pointer-events:none and hides on mouseleave, so anything laid past + the old fixed 22rem max-height could never be scrolled to or read. + The JS side now bounds the content by REAL MEASUREMENT (trim lines + until the laid-out content fits + a truthful elision row), and the + height cap is viewport-relative — the #240 defect-D width-cap + pattern — so positionTipNear's post-flip top clamp always fully + contains the box on the height axis, at any viewport height. + overflow:auto stays as the safety net (PR #286 review): if some + exotic rendering still defeats the measured trim, the mismatch + degrades to a visible scrollbar — a signal — never a silent clip. */ + max-height: calc(100vh - 16px); overflow: auto; border: var(--rule-w) solid var(--border-strong); border-radius: var(--radius-pan); box-shadow: 0 8px 26px rgba(0,0,0,0.34); background: var(--code-bg); pointer-events: none; @@ -1738,6 +1750,11 @@ .fmt-tooltip .code-header { font-size: 0.74rem; } .fmt-tooltip .sql-block { margin: 0; padding: 0.4rem 0; } .fmt-tooltip .code-line { padding-right: 1rem; } +/* cute-dbt#246 — the truthful elision row under the trimmed block. Plain + var(--text) on the bubble's var(--code-bg) (the code-line pairing the + 8-theme AA matrix already covers); fixed px so the Sakura 62.5% root + (1rem = 10px) cannot shrink it unreadable. */ +.fmt-tooltip .fmt-more { padding: 0.15rem 0.8rem 0.4rem; font-size: 11px; color: var(--text); } /* ============================================================================ 6. ENGINE-PR ADDITIONS (cute-dbt#178) @@ -7593,16 +7610,59 @@

Expected

} // Format tip: the fixture reconstructed in its authored format, in a // framed mini code-surface (sql/yaml ride the #132 highlighters). + // + // cute-dbt#246 — the bubble is pointer-events:none and hides on + // mouseleave/blur, so anything laid below a height fold could never be + // scrolled to or read (the old fixed 22rem + overflow:auto pairing left + // below-fold rows silently unreachable for EVERY input modality). Bound + // the content instead of folding it: show at most FMT_TIP_MAX_LINES + // lines — the tip is a glance surface; the grid below the badge + // carries the full data — then trim by REAL MEASUREMENT (PR #286 + // review): remove rendered lines until the laid-out content fits the + // bubble's viewport-capped box. Measuring the live layout (never a + // px-per-line estimate) keeps the bound correct under browser zoom, a + // forced minimum font size (a11y), wrapped long lines, and any future + // rem-based sizing — and the truthful elision row counts exactly what + // was trimmed, however it was trimmed. + var FMT_TIP_MAX_LINES = 20; + function fmtTipMoreText(more) { + return "… +" + more + " more line" + (more === 1 ? "" : "s"); + } function showFmtTip(trigger) { var fmt = trigger.getAttribute("data-fmt"), lang = trigger.getAttribute("data-fmt-lang"); var block = trigger.getAttribute("data-fmt-block") || ""; - var code = lang === "sql" ? highlightLinesSql(block) - : lang === "yaml" ? highlightLinesYaml(block) - : highlightLinesPlain(block); + var lines = String(block).replace(/\n+$/, "").split("\n"); + var shown = Math.min(lines.length, FMT_TIP_MAX_LINES); + var more = lines.length - shown; + var code = lang === "sql" ? highlightLinesSql(lines.slice(0, shown).join("\n")) + : lang === "yaml" ? highlightLinesYaml(lines.slice(0, shown).join("\n")) + : highlightLinesPlain(lines.slice(0, shown).join("\n")); var el = ensureTip("fmt-tooltip", "fmt-tooltip"); el.innerHTML = '
format: ' + escapeHtml(fmt) - + '
' + code + "
"; + + '
' + code + "
" + + (more > 0 ? '
' + fmtTipMoreText(more) + "
" : ""); el.hidden = false; + // Measure the live layout at full width (reset a stale edge-clamped + // left BEFORE measuring — the #232 D18 lesson: a squeezed box wraps + // differently and mis-measures). + el.style.left = "0px"; + // The elision row participates in the measured height, so it must + // exist before trimming starts (created here if the ceiling alone + // didn't already call for one). + var moreEl = el.querySelector(".fmt-more"); + if (!moreEl && el.scrollHeight > el.clientHeight + 1) { + moreEl = document.createElement("div"); + moreEl.className = "fmt-more"; + el.appendChild(moreEl); + } + var rows = el.querySelectorAll(".code-line"); + var keep = rows.length; + while (keep > 1 && el.scrollHeight > el.clientHeight + 1) { + keep -= 1; + rows[keep].parentNode.removeChild(rows[keep]); + more += 1; + } + if (moreEl) moreEl.textContent = fmtTipMoreText(more); positionTipNear(el, trigger); } // Overrides tip: key = value rows grouped by macros / vars / env_vars. diff --git a/examples/jaffle-shop-report.html b/examples/jaffle-shop-report.html index 0758a390..28e53830 100644 --- a/examples/jaffle-shop-report.html +++ b/examples/jaffle-shop-report.html @@ -1730,7 +1730,19 @@ surface (NOT the dark .col-tooltip shell — it reuses the code-card chrome so the reconstruction reads like the drawers). */ .fmt-tooltip { - position: fixed; z-index: 90; max-width: min(34rem, 92vw); max-height: 22rem; overflow: auto; + position: fixed; z-index: 90; max-width: min(34rem, 92vw); + /* cute-dbt#246 — never a reachability fold: the bubble is + pointer-events:none and hides on mouseleave, so anything laid past + the old fixed 22rem max-height could never be scrolled to or read. + The JS side now bounds the content by REAL MEASUREMENT (trim lines + until the laid-out content fits + a truthful elision row), and the + height cap is viewport-relative — the #240 defect-D width-cap + pattern — so positionTipNear's post-flip top clamp always fully + contains the box on the height axis, at any viewport height. + overflow:auto stays as the safety net (PR #286 review): if some + exotic rendering still defeats the measured trim, the mismatch + degrades to a visible scrollbar — a signal — never a silent clip. */ + max-height: calc(100vh - 16px); overflow: auto; border: var(--rule-w) solid var(--border-strong); border-radius: var(--radius-pan); box-shadow: 0 8px 26px rgba(0,0,0,0.34); background: var(--code-bg); pointer-events: none; @@ -1738,6 +1750,11 @@ .fmt-tooltip .code-header { font-size: 0.74rem; } .fmt-tooltip .sql-block { margin: 0; padding: 0.4rem 0; } .fmt-tooltip .code-line { padding-right: 1rem; } +/* cute-dbt#246 — the truthful elision row under the trimmed block. Plain + var(--text) on the bubble's var(--code-bg) (the code-line pairing the + 8-theme AA matrix already covers); fixed px so the Sakura 62.5% root + (1rem = 10px) cannot shrink it unreadable. */ +.fmt-tooltip .fmt-more { padding: 0.15rem 0.8rem 0.4rem; font-size: 11px; color: var(--text); } /* ============================================================================ 6. ENGINE-PR ADDITIONS (cute-dbt#178) @@ -7528,16 +7545,59 @@

Expected

} // Format tip: the fixture reconstructed in its authored format, in a // framed mini code-surface (sql/yaml ride the #132 highlighters). + // + // cute-dbt#246 — the bubble is pointer-events:none and hides on + // mouseleave/blur, so anything laid below a height fold could never be + // scrolled to or read (the old fixed 22rem + overflow:auto pairing left + // below-fold rows silently unreachable for EVERY input modality). Bound + // the content instead of folding it: show at most FMT_TIP_MAX_LINES + // lines — the tip is a glance surface; the grid below the badge + // carries the full data — then trim by REAL MEASUREMENT (PR #286 + // review): remove rendered lines until the laid-out content fits the + // bubble's viewport-capped box. Measuring the live layout (never a + // px-per-line estimate) keeps the bound correct under browser zoom, a + // forced minimum font size (a11y), wrapped long lines, and any future + // rem-based sizing — and the truthful elision row counts exactly what + // was trimmed, however it was trimmed. + var FMT_TIP_MAX_LINES = 20; + function fmtTipMoreText(more) { + return "… +" + more + " more line" + (more === 1 ? "" : "s"); + } function showFmtTip(trigger) { var fmt = trigger.getAttribute("data-fmt"), lang = trigger.getAttribute("data-fmt-lang"); var block = trigger.getAttribute("data-fmt-block") || ""; - var code = lang === "sql" ? highlightLinesSql(block) - : lang === "yaml" ? highlightLinesYaml(block) - : highlightLinesPlain(block); + var lines = String(block).replace(/\n+$/, "").split("\n"); + var shown = Math.min(lines.length, FMT_TIP_MAX_LINES); + var more = lines.length - shown; + var code = lang === "sql" ? highlightLinesSql(lines.slice(0, shown).join("\n")) + : lang === "yaml" ? highlightLinesYaml(lines.slice(0, shown).join("\n")) + : highlightLinesPlain(lines.slice(0, shown).join("\n")); var el = ensureTip("fmt-tooltip", "fmt-tooltip"); el.innerHTML = '
format: ' + escapeHtml(fmt) - + '
' + code + "
"; + + '
' + code + "
" + + (more > 0 ? '
' + fmtTipMoreText(more) + "
" : ""); el.hidden = false; + // Measure the live layout at full width (reset a stale edge-clamped + // left BEFORE measuring — the #232 D18 lesson: a squeezed box wraps + // differently and mis-measures). + el.style.left = "0px"; + // The elision row participates in the measured height, so it must + // exist before trimming starts (created here if the ceiling alone + // didn't already call for one). + var moreEl = el.querySelector(".fmt-more"); + if (!moreEl && el.scrollHeight > el.clientHeight + 1) { + moreEl = document.createElement("div"); + moreEl.className = "fmt-more"; + el.appendChild(moreEl); + } + var rows = el.querySelectorAll(".code-line"); + var keep = rows.length; + while (keep > 1 && el.scrollHeight > el.clientHeight + 1) { + keep -= 1; + rows[keep].parentNode.removeChild(rows[keep]); + more += 1; + } + if (moreEl) moreEl.textContent = fmtTipMoreText(more); positionTipNear(el, trigger); } // Overrides tip: key = value rows grouped by macros / vars / env_vars. diff --git a/examples/playground-report.html b/examples/playground-report.html index 585db232..14507717 100644 --- a/examples/playground-report.html +++ b/examples/playground-report.html @@ -1730,7 +1730,19 @@ surface (NOT the dark .col-tooltip shell — it reuses the code-card chrome so the reconstruction reads like the drawers). */ .fmt-tooltip { - position: fixed; z-index: 90; max-width: min(34rem, 92vw); max-height: 22rem; overflow: auto; + position: fixed; z-index: 90; max-width: min(34rem, 92vw); + /* cute-dbt#246 — never a reachability fold: the bubble is + pointer-events:none and hides on mouseleave, so anything laid past + the old fixed 22rem max-height could never be scrolled to or read. + The JS side now bounds the content by REAL MEASUREMENT (trim lines + until the laid-out content fits + a truthful elision row), and the + height cap is viewport-relative — the #240 defect-D width-cap + pattern — so positionTipNear's post-flip top clamp always fully + contains the box on the height axis, at any viewport height. + overflow:auto stays as the safety net (PR #286 review): if some + exotic rendering still defeats the measured trim, the mismatch + degrades to a visible scrollbar — a signal — never a silent clip. */ + max-height: calc(100vh - 16px); overflow: auto; border: var(--rule-w) solid var(--border-strong); border-radius: var(--radius-pan); box-shadow: 0 8px 26px rgba(0,0,0,0.34); background: var(--code-bg); pointer-events: none; @@ -1738,6 +1750,11 @@ .fmt-tooltip .code-header { font-size: 0.74rem; } .fmt-tooltip .sql-block { margin: 0; padding: 0.4rem 0; } .fmt-tooltip .code-line { padding-right: 1rem; } +/* cute-dbt#246 — the truthful elision row under the trimmed block. Plain + var(--text) on the bubble's var(--code-bg) (the code-line pairing the + 8-theme AA matrix already covers); fixed px so the Sakura 62.5% root + (1rem = 10px) cannot shrink it unreadable. */ +.fmt-tooltip .fmt-more { padding: 0.15rem 0.8rem 0.4rem; font-size: 11px; color: var(--text); } /* ============================================================================ 6. ENGINE-PR ADDITIONS (cute-dbt#178) @@ -7528,16 +7545,59 @@

Expected

} // Format tip: the fixture reconstructed in its authored format, in a // framed mini code-surface (sql/yaml ride the #132 highlighters). + // + // cute-dbt#246 — the bubble is pointer-events:none and hides on + // mouseleave/blur, so anything laid below a height fold could never be + // scrolled to or read (the old fixed 22rem + overflow:auto pairing left + // below-fold rows silently unreachable for EVERY input modality). Bound + // the content instead of folding it: show at most FMT_TIP_MAX_LINES + // lines — the tip is a glance surface; the grid below the badge + // carries the full data — then trim by REAL MEASUREMENT (PR #286 + // review): remove rendered lines until the laid-out content fits the + // bubble's viewport-capped box. Measuring the live layout (never a + // px-per-line estimate) keeps the bound correct under browser zoom, a + // forced minimum font size (a11y), wrapped long lines, and any future + // rem-based sizing — and the truthful elision row counts exactly what + // was trimmed, however it was trimmed. + var FMT_TIP_MAX_LINES = 20; + function fmtTipMoreText(more) { + return "… +" + more + " more line" + (more === 1 ? "" : "s"); + } function showFmtTip(trigger) { var fmt = trigger.getAttribute("data-fmt"), lang = trigger.getAttribute("data-fmt-lang"); var block = trigger.getAttribute("data-fmt-block") || ""; - var code = lang === "sql" ? highlightLinesSql(block) - : lang === "yaml" ? highlightLinesYaml(block) - : highlightLinesPlain(block); + var lines = String(block).replace(/\n+$/, "").split("\n"); + var shown = Math.min(lines.length, FMT_TIP_MAX_LINES); + var more = lines.length - shown; + var code = lang === "sql" ? highlightLinesSql(lines.slice(0, shown).join("\n")) + : lang === "yaml" ? highlightLinesYaml(lines.slice(0, shown).join("\n")) + : highlightLinesPlain(lines.slice(0, shown).join("\n")); var el = ensureTip("fmt-tooltip", "fmt-tooltip"); el.innerHTML = '
format: ' + escapeHtml(fmt) - + '
' + code + "
"; + + '
' + code + "
" + + (more > 0 ? '
' + fmtTipMoreText(more) + "
" : ""); el.hidden = false; + // Measure the live layout at full width (reset a stale edge-clamped + // left BEFORE measuring — the #232 D18 lesson: a squeezed box wraps + // differently and mis-measures). + el.style.left = "0px"; + // The elision row participates in the measured height, so it must + // exist before trimming starts (created here if the ceiling alone + // didn't already call for one). + var moreEl = el.querySelector(".fmt-more"); + if (!moreEl && el.scrollHeight > el.clientHeight + 1) { + moreEl = document.createElement("div"); + moreEl.className = "fmt-more"; + el.appendChild(moreEl); + } + var rows = el.querySelectorAll(".code-line"); + var keep = rows.length; + while (keep > 1 && el.scrollHeight > el.clientHeight + 1) { + keep -= 1; + rows[keep].parentNode.removeChild(rows[keep]); + more += 1; + } + if (moreEl) moreEl.textContent = fmtTipMoreText(more); positionTipNear(el, trigger); } // Overrides tip: key = value rows grouped by macros / vars / env_vars. diff --git a/templates/interaction.js b/templates/interaction.js index 3bde39c7..eb59e202 100644 --- a/templates/interaction.js +++ b/templates/interaction.js @@ -1761,16 +1761,59 @@ } // Format tip: the fixture reconstructed in its authored format, in a // framed mini code-surface (sql/yaml ride the #132 highlighters). + // + // cute-dbt#246 — the bubble is pointer-events:none and hides on + // mouseleave/blur, so anything laid below a height fold could never be + // scrolled to or read (the old fixed 22rem + overflow:auto pairing left + // below-fold rows silently unreachable for EVERY input modality). Bound + // the content instead of folding it: show at most FMT_TIP_MAX_LINES + // lines — the tip is a glance surface; the grid below the badge + // carries the full data — then trim by REAL MEASUREMENT (PR #286 + // review): remove rendered lines until the laid-out content fits the + // bubble's viewport-capped box. Measuring the live layout (never a + // px-per-line estimate) keeps the bound correct under browser zoom, a + // forced minimum font size (a11y), wrapped long lines, and any future + // rem-based sizing — and the truthful elision row counts exactly what + // was trimmed, however it was trimmed. + var FMT_TIP_MAX_LINES = 20; + function fmtTipMoreText(more) { + return "… +" + more + " more line" + (more === 1 ? "" : "s"); + } function showFmtTip(trigger) { var fmt = trigger.getAttribute("data-fmt"), lang = trigger.getAttribute("data-fmt-lang"); var block = trigger.getAttribute("data-fmt-block") || ""; - var code = lang === "sql" ? highlightLinesSql(block) - : lang === "yaml" ? highlightLinesYaml(block) - : highlightLinesPlain(block); + var lines = String(block).replace(/\n+$/, "").split("\n"); + var shown = Math.min(lines.length, FMT_TIP_MAX_LINES); + var more = lines.length - shown; + var code = lang === "sql" ? highlightLinesSql(lines.slice(0, shown).join("\n")) + : lang === "yaml" ? highlightLinesYaml(lines.slice(0, shown).join("\n")) + : highlightLinesPlain(lines.slice(0, shown).join("\n")); var el = ensureTip("fmt-tooltip", "fmt-tooltip"); el.innerHTML = '
format: ' + escapeHtml(fmt) - + '
' + code + "
"; + + '
' + code + "
" + + (more > 0 ? '
' + fmtTipMoreText(more) + "
" : ""); el.hidden = false; + // Measure the live layout at full width (reset a stale edge-clamped + // left BEFORE measuring — the #232 D18 lesson: a squeezed box wraps + // differently and mis-measures). + el.style.left = "0px"; + // The elision row participates in the measured height, so it must + // exist before trimming starts (created here if the ceiling alone + // didn't already call for one). + var moreEl = el.querySelector(".fmt-more"); + if (!moreEl && el.scrollHeight > el.clientHeight + 1) { + moreEl = document.createElement("div"); + moreEl.className = "fmt-more"; + el.appendChild(moreEl); + } + var rows = el.querySelectorAll(".code-line"); + var keep = rows.length; + while (keep > 1 && el.scrollHeight > el.clientHeight + 1) { + keep -= 1; + rows[keep].parentNode.removeChild(rows[keep]); + more += 1; + } + if (moreEl) moreEl.textContent = fmtTipMoreText(more); positionTipNear(el, trigger); } // Overrides tip: key = value rows grouped by macros / vars / env_vars. diff --git a/templates/report.css b/templates/report.css index 64b022c0..deee9cc9 100644 --- a/templates/report.css +++ b/templates/report.css @@ -1044,7 +1044,19 @@ th.has-col-meta:focus-visible { outline: 2px solid var(--accent); outline-offset surface (NOT the dark .col-tooltip shell — it reuses the code-card chrome so the reconstruction reads like the drawers). */ .fmt-tooltip { - position: fixed; z-index: 90; max-width: min(34rem, 92vw); max-height: 22rem; overflow: auto; + position: fixed; z-index: 90; max-width: min(34rem, 92vw); + /* cute-dbt#246 — never a reachability fold: the bubble is + pointer-events:none and hides on mouseleave, so anything laid past + the old fixed 22rem max-height could never be scrolled to or read. + The JS side now bounds the content by REAL MEASUREMENT (trim lines + until the laid-out content fits + a truthful elision row), and the + height cap is viewport-relative — the #240 defect-D width-cap + pattern — so positionTipNear's post-flip top clamp always fully + contains the box on the height axis, at any viewport height. + overflow:auto stays as the safety net (PR #286 review): if some + exotic rendering still defeats the measured trim, the mismatch + degrades to a visible scrollbar — a signal — never a silent clip. */ + max-height: calc(100vh - 16px); overflow: auto; border: var(--rule-w) solid var(--border-strong); border-radius: var(--radius-pan); box-shadow: 0 8px 26px rgba(0,0,0,0.34); background: var(--code-bg); pointer-events: none; @@ -1052,6 +1064,11 @@ th.has-col-meta:focus-visible { outline: 2px solid var(--accent); outline-offset .fmt-tooltip .code-header { font-size: 0.74rem; } .fmt-tooltip .sql-block { margin: 0; padding: 0.4rem 0; } .fmt-tooltip .code-line { padding-right: 1rem; } +/* cute-dbt#246 — the truthful elision row under the trimmed block. Plain + var(--text) on the bubble's var(--code-bg) (the code-line pairing the + 8-theme AA matrix already covers); fixed px so the Sakura 62.5% root + (1rem = 10px) cannot shrink it unreadable. */ +.fmt-tooltip .fmt-more { padding: 0.15rem 0.8rem 0.4rem; font-size: 11px; color: var(--text); } /* ============================================================================ 6. ENGINE-PR ADDITIONS (cute-dbt#178) diff --git a/tests/headless_toggle.rs b/tests/headless_toggle.rs index 10cd81f7..cbd94524 100644 --- a/tests/headless_toggle.rs +++ b/tests/headless_toggle.rs @@ -9877,16 +9877,20 @@ fn ov_tip_long_value_contained_in_bubble_and_viewport_at_right_edge() { "the overrides tip CONTAINS its content (the long value wraps \ inside the painted background instead of painting past it)", ); - // Pin 2 — viewport containment at the right-edge trigger. The top - // bound also pins the PR #244-review post-flip top clamp (the full - // pathological-viewport-height pin rides cute-dbt#246). + // Pin 2 — viewport containment at the right-edge trigger, on BOTH + // axes (cute-dbt#246 extended the guard to the height axis; the top + // bound also pins the PR #244-review post-flip top clamp, and the + // full pathological-viewport-height pin lives in + // `fmt_tip_contained_at_pathological_viewport_height`). assert!( eval_bool( &tab, &format!( "(function(){{var r={TIP}.getBoundingClientRect();\ var vw=document.documentElement.clientWidth;\ - return r.top >= 0 && r.left >= 0 && r.right <= vw + 1;}})()" + var vh=document.documentElement.clientHeight;\ + return r.top >= 0 && r.bottom <= vh + 1 \ + && r.left >= 0 && r.right <= vw + 1;}})()" ), ), "the overrides bubble's box lies fully within the visible viewport \ @@ -9911,6 +9915,297 @@ fn ov_tip_long_value_contained_in_bubble_and_viewport_at_right_edge() { let _ = tab.close(true); } +/// A long unit-test fixture for the cute-dbt#246 fmt-tip reachability +/// pins: 30 dict rows × 2 columns reconstruct as 60 YAML lines — far +/// past any realistic bubble fold. +fn long_fixture_unit_test() -> UnitTest { + let rows: Vec = (1..=30) + .map(|i| serde_json::json!({ "id": i, "amount": i * 10 })) + .collect(); + UnitTest::new( + "longfix".to_owned(), + NodeId::new("dim_x"), + vec![UnitTestGiven::new( + "ref('src')".to_owned(), + serde_json::Value::Array(rows), + Some("dict".to_owned()), + None, + )], + UnitTestExpect::new( + serde_json::json!([{ "id": 1 }]), + Some("dict".to_owned()), + None, + ), + None, + DependsOn::default(), + None, + None, + None, + ) +} + +#[test] +#[ignore = "requires Chrome; runs explicitly in the headless-zero-egress CI job via `-- --ignored`"] +fn fmt_tip_long_fixture_is_bounded_not_folded() { + // cute-dbt#246 (bullet 1) — #fmt-tooltip is pointer-events:none and + // hides on mouseleave/blur, so any content laid below its max-height + // fold (the old fixed 22rem + overflow:auto pairing) could never be + // scrolled to or read — silently unreachable for EVERY input + // modality. RED pre-fix with this 60-line reconstruction (content + // ~3× the fold); the fix bounds the content (measurement-driven line + // trim + a truthful elision row) so the fold cannot occur. + let url = render_to_file( + "headless_246_fmt_tip_long.html", + vec![model_node("model.shop.dim_x"), model_node("model.shop.src")], + vec![("unit_test.shop.dim_x.longfix", long_fixture_unit_test())], + &["model.shop.dim_x"], + &[], + ); + let browser = launch_browser(); + let tab = browser.new_tab().expect("new tab"); + tab.navigate_to(&url).expect("navigate"); + tab.wait_until_navigated().expect("await navigation"); + select_model(&tab, "dim_x"); + + const BADGE: &str = "document.querySelector('.given-section .format-badge.has-fmt-tip')"; + const TIP: &str = "document.getElementById('fmt-tooltip')"; + let _ = eval(&tab, &format!("{BADGE}.focus()")); + assert!( + !eval_bool(&tab, &format!("{TIP}.hidden")), + "focusing the format badge reveals the fmt tip", + ); + // The reachability pin (RED pre-fix): no fold — everything the bubble + // holds paints inside its own painted box (1px tolerance, the repo + // convention). A fold here is unreachable by definition: the bubble + // is pointer-events:none and hides the moment the trigger is left. + assert!( + eval_bool( + &tab, + &format!("{TIP}.scrollHeight <= {TIP}.clientHeight + 1") + ), + "the fmt tip CONTAINS its content — no below-fold rows (a fold is \ + unreachable for every input modality: pointer-events:none + \ + hide-on-mouseleave)", + ); + // The bound is truthful, not silent: a visible elision row counts + // exactly what was trimmed (shown + counted == the 60 authored + // lines). Relational pins, not exact counts — the budget is derived + // from the live viewport height, and the headless default window + // maps to different viewport heights per platform (macOS reserves + // window chrome; Linux does not). + assert!( + eval_bool( + &tab, + &format!( + "(function(){{var shown={TIP}.querySelectorAll('.diff-code').length;\ + var m={TIP}.querySelector('.fmt-more');\ + if (!m) return false;\ + var n=parseInt(m.textContent.replace(/[^0-9]/g, ''), 10);\ + return shown > 0 && shown <= 20 && shown + n === 60;}})()" + ), + ), + "the elision row truthfully accounts for every trimmed line \ + (shown + counted === 60, within the 20-line ceiling)", + ); + assert!( + eval_string( + &tab, + &format!("{TIP}.querySelector('.fmt-more').textContent") + ) + .starts_with('\u{2026}'), + "the elision row reads as an ellipsis line", + ); + // The trim takes the HEAD of the reconstruction (sorted dict keys: + // amount precedes id), never a mid-block slice. + assert_eq!( + eval_string( + &tab, + &format!("{TIP}.querySelector('.diff-code').textContent") + ), + "- amount: 10", + "the first shown line is the head of the authored reconstruction", + ); + // And the box itself lies fully inside the visible viewport. + assert!( + eval_bool( + &tab, + &format!( + "(function(){{var r={TIP}.getBoundingClientRect();\ + var vw=document.documentElement.clientWidth;\ + var vh=document.documentElement.clientHeight;\ + return r.top >= 0 && r.bottom <= vh + 1 \ + && r.left >= 0 && r.right <= vw + 1;}})()" + ), + ), + "the fmt tip's box lies fully within the visible viewport", + ); + + // ===== font-scale phase (PR #286 review) ===== + // A forced minimum font size (Chrome a11y) inflates the px-sized + // .sql-block lines past any per-line estimate; the bound must come + // from measuring the live layout, never constants. Simulate the + // inflation (12px → 24px doubles every line box), re-show the tip, + // and re-assert the same truths: no fold, truthful elision. + let shown_normal = eval( + &tab, + &format!("{TIP}.querySelectorAll('.diff-code').length"), + ) + .as_i64() + .expect("shown line count is a number"); + let _ = eval(&tab, &format!("{BADGE}.blur()")); + let _ = eval( + &tab, + "(function(){var s=document.createElement('style');\ + s.textContent='.fmt-tooltip .sql-block { font-size: 24px; }';\ + document.head.appendChild(s);})()", + ); + let _ = eval(&tab, &format!("{BADGE}.focus()")); + assert!( + !eval_bool(&tab, &format!("{TIP}.hidden")), + "the fmt tip re-reveals under the inflated font", + ); + assert!( + eval_bool( + &tab, + &format!("{TIP}.scrollHeight <= {TIP}.clientHeight + 1") + ), + "no fold under a forced larger font — the measured trim adapts \ + (an estimate-based budget would overshoot and clip)", + ); + assert!( + eval_bool( + &tab, + &format!( + "(function(){{var shown={TIP}.querySelectorAll('.diff-code').length;\ + var m={TIP}.querySelector('.fmt-more');\ + if (!m) return false;\ + var n=parseInt(m.textContent.replace(/[^0-9]/g, ''), 10);\ + return shown > 0 && shown <= 20 && shown + n === 60;}})()" + ), + ), + "the elision row stays truthful under the inflated font \ + (shown + counted === 60)", + ); + // Non-vacuous: doubled line boxes must fit FEWER lines — proof the + // trim measured the new layout rather than reusing a constant. + let shown_scaled = eval( + &tab, + &format!("{TIP}.querySelectorAll('.diff-code').length"), + ) + .as_i64() + .expect("shown line count is a number"); + assert!( + shown_scaled < shown_normal, + "the measured trim adapts to the inflated font \ + ({shown_scaled} lines shown vs {shown_normal} at normal size)", + ); + + let _ = tab.close(true); +} + +#[test] +#[ignore = "requires Chrome; runs explicitly in the headless-zero-egress CI job via `-- --ignored`"] +fn fmt_tip_contained_at_pathological_viewport_height() { + // cute-dbt#246 (bullet 2's remaining pin) — the full pathological- + // viewport-height containment pin promised by the PR #244 guard + // comment: at pathological viewport heights the flip-above branch + // used to park bubbles at negative top (clamped since #244), and the + // fixed 22rem max-height kept the box taller than the screen (bottom + // far past the viewport — RED pre-fix). The viewport-relative height + // cap + the post-flip top clamp must fully contain the box on the + // height axis even here; contained beats clipped. + // + // Window-size → viewport mapping is PLATFORM-DEPENDENT (CDP + // setDeviceMetricsOverride is a documented no-op in headless_chrome + // 1.0.21, see the #157 viewport test, so the launch window is the + // only lever): macOS floors the outer window at ~375px and reserves + // ~143px of chrome (a ~111px request measured a 232px viewport), + // while the Linux CI runner HONORS tiny requests (the same ~111px + // request measured a 24px viewport — too short to even hold the + // 1-line budget floor plus the bubble chrome). A ~300px request + // lands BOTH platforms in a comparable pathological-but-sane band + // (macOS ≈232px via its floor; Linux ≈213-300px depending on chrome + // height). Every pin below therefore measures against the LIVE + // clientHeight, and the premise assert keeps the resulting viewport + // inside that band — well under the ~440px a full 20-line budget + // needs, so the height-axis pins are exercised for real (RED + // pre-fix here via the no-fold pin: the old fixed 222px box — 22rem + // at the Sakura 10px root + borders — folded 1178px of content). + let url = render_to_file( + "headless_246_fmt_tip_pathological.html", + vec![model_node("model.shop.dim_x"), model_node("model.shop.src")], + vec![("unit_test.shop.dim_x.longfix", long_fixture_unit_test())], + &["model.shop.dim_x"], + &[], + ); + let browser = launch_browser_sized(Some((900, 300))); + let tab = browser.new_tab().expect("new tab"); + tab.navigate_to(&url).expect("navigate"); + tab.wait_until_navigated().expect("await navigation"); + select_model(&tab, "dim_x"); + + const BADGE: &str = "document.querySelector('.given-section .format-badge.has-fmt-tip')"; + const TIP: &str = "document.getElementById('fmt-tooltip')"; + // Premise: the launch produced a pathologically short viewport on + // this platform's window mapping, so the pins below cannot pass as + // a tall-viewport no-op. + let vh = eval(&tab, "document.documentElement.clientHeight") + .as_i64() + .expect("clientHeight is a number"); + assert!( + (100..=352).contains(&vh), + "premise: the pathological launch yields a short viewport \ + (got {vh}px; expected the ~300px request to land in the \ + 100..=352 band on every platform's window-chrome mapping)", + ); + // focus() scrolls the trigger into the (tiny) viewport, so the + // singleton positions off a real on-screen rect — the keyboard path. + let _ = eval(&tab, &format!("{BADGE}.focus()")); + assert!( + !eval_bool(&tab, &format!("{TIP}.hidden")), + "focusing the format badge reveals the fmt tip at the pathological height", + ); + assert!( + eval_bool( + &tab, + &format!( + "(function(){{var r={TIP}.getBoundingClientRect();\ + var vh=document.documentElement.clientHeight;\ + return r.top >= 0 && r.bottom <= vh + 1;}})()" + ), + ), + "the fmt tip is contained on the height axis at a pathological \ + viewport height (post-flip top clamp + viewport-relative height cap)", + ); + // No fold even here: the viewport-derived line budget keeps the + // content inside the capped box. + assert!( + eval_bool( + &tab, + &format!("{TIP}.scrollHeight <= {TIP}.clientHeight + 1") + ), + "the fmt tip contains its content with no fold at the pathological height", + ); + // The elision stays truthful at whatever budget the platform's floor + // produced: shown lines + the counted trim == the 60 authored lines. + assert!( + eval_bool( + &tab, + &format!( + "(function(){{var shown={TIP}.querySelectorAll('.diff-code').length;\ + var m={TIP}.querySelector('.fmt-more');\ + if (!m) return false;\ + var n=parseInt(m.textContent.replace(/[^0-9]/g, ''), 10);\ + return shown < 60 && shown + n === 60;}})()" + ), + ), + "the elision row truthfully accounts for every trimmed line \ + (shown + counted === 60)", + ); + + let _ = tab.close(true); +} + /// cute-dbt#240 (defect C) — `.finding-covered-by code` AA sweep across /// all 8 themes. Sakura's `code {{ background:#f1f1f1 }}` paired with the /// near-white dark-theme `--text` at ≈1.2:1 (the founder's "near-white diff --git a/tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap b/tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap index 58fff440..c7d5553a 100644 --- a/tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap +++ b/tests/snapshots/render_integration__rendered_chrome_jaffle_shop.snap @@ -1508,7 +1508,19 @@ th.has-col-meta:focus-visible { outline: 2px solid var(--accent); outline-offset surface (NOT the dark .col-tooltip shell — it reuses the code-card chrome so the reconstruction reads like the drawers). */ .fmt-tooltip { - position: fixed; z-index: 90; max-width: min(34rem, 92vw); max-height: 22rem; overflow: auto; + position: fixed; z-index: 90; max-width: min(34rem, 92vw); + /* cute-dbt#246 — never a reachability fold: the bubble is + pointer-events:none and hides on mouseleave, so anything laid past + the old fixed 22rem max-height could never be scrolled to or read. + The JS side now bounds the content by REAL MEASUREMENT (trim lines + until the laid-out content fits + a truthful elision row), and the + height cap is viewport-relative — the #240 defect-D width-cap + pattern — so positionTipNear's post-flip top clamp always fully + contains the box on the height axis, at any viewport height. + overflow:auto stays as the safety net (PR #286 review): if some + exotic rendering still defeats the measured trim, the mismatch + degrades to a visible scrollbar — a signal — never a silent clip. */ + max-height: calc(100vh - 16px); overflow: auto; border: var(--rule-w) solid var(--border-strong); border-radius: var(--radius-pan); box-shadow: 0 8px 26px rgba(0,0,0,0.34); background: var(--code-bg); pointer-events: none; @@ -1516,6 +1528,11 @@ th.has-col-meta:focus-visible { outline: 2px solid var(--accent); outline-offset .fmt-tooltip .code-header { font-size: 0.74rem; } .fmt-tooltip .sql-block { margin: 0; padding: 0.4rem 0; } .fmt-tooltip .code-line { padding-right: 1rem; } +/* cute-dbt#246 — the truthful elision row under the trimmed block. Plain + var(--text) on the bubble's var(--code-bg) (the code-line pairing the + 8-theme AA matrix already covers); fixed px so the Sakura 62.5% root + (1rem = 10px) cannot shrink it unreadable. */ +.fmt-tooltip .fmt-more { padding: 0.15rem 0.8rem 0.4rem; font-size: 11px; color: var(--text); } /* ============================================================================ 6. ENGINE-PR ADDITIONS (cute-dbt#178) @@ -3864,16 +3881,59 @@ html[data-style="swiss"] .degraded-chip { border-radius: 0; } } // Format tip: the fixture reconstructed in its authored format, in a // framed mini code-surface (sql/yaml ride the #132 highlighters). + // + // cute-dbt#246 — the bubble is pointer-events:none and hides on + // mouseleave/blur, so anything laid below a height fold could never be + // scrolled to or read (the old fixed 22rem + overflow:auto pairing left + // below-fold rows silently unreachable for EVERY input modality). Bound + // the content instead of folding it: show at most FMT_TIP_MAX_LINES + // lines — the tip is a glance surface; the grid below the badge + // carries the full data — then trim by REAL MEASUREMENT (PR #286 + // review): remove rendered lines until the laid-out content fits the + // bubble's viewport-capped box. Measuring the live layout (never a + // px-per-line estimate) keeps the bound correct under browser zoom, a + // forced minimum font size (a11y), wrapped long lines, and any future + // rem-based sizing — and the truthful elision row counts exactly what + // was trimmed, however it was trimmed. + var FMT_TIP_MAX_LINES = 20; + function fmtTipMoreText(more) { + return "… +" + more + " more line" + (more === 1 ? "" : "s"); + } function showFmtTip(trigger) { var fmt = trigger.getAttribute("data-fmt"), lang = trigger.getAttribute("data-fmt-lang"); var block = trigger.getAttribute("data-fmt-block") || ""; - var code = lang === "sql" ? highlightLinesSql(block) - : lang === "yaml" ? highlightLinesYaml(block) - : highlightLinesPlain(block); + var lines = String(block).replace(/\n+$/, "").split("\n"); + var shown = Math.min(lines.length, FMT_TIP_MAX_LINES); + var more = lines.length - shown; + var code = lang === "sql" ? highlightLinesSql(lines.slice(0, shown).join("\n")) + : lang === "yaml" ? highlightLinesYaml(lines.slice(0, shown).join("\n")) + : highlightLinesPlain(lines.slice(0, shown).join("\n")); var el = ensureTip("fmt-tooltip", "fmt-tooltip"); el.innerHTML = '
format: ' + escapeHtml(fmt) - + '
' + code + "
"; + + '
' + code + "
" + + (more > 0 ? '
' + fmtTipMoreText(more) + "
" : ""); el.hidden = false; + // Measure the live layout at full width (reset a stale edge-clamped + // left BEFORE measuring — the #232 D18 lesson: a squeezed box wraps + // differently and mis-measures). + el.style.left = "0px"; + // The elision row participates in the measured height, so it must + // exist before trimming starts (created here if the ceiling alone + // didn't already call for one). + var moreEl = el.querySelector(".fmt-more"); + if (!moreEl && el.scrollHeight > el.clientHeight + 1) { + moreEl = document.createElement("div"); + moreEl.className = "fmt-more"; + el.appendChild(moreEl); + } + var rows = el.querySelectorAll(".code-line"); + var keep = rows.length; + while (keep > 1 && el.scrollHeight > el.clientHeight + 1) { + keep -= 1; + rows[keep].parentNode.removeChild(rows[keep]); + more += 1; + } + if (moreEl) moreEl.textContent = fmtTipMoreText(more); positionTipNear(el, trigger); } // Overrides tip: key = value rows grouped by macros / vars / env_vars.