<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Spec on Tarragon</title><link>https://tarrragon.github.io/blog/tags/spec/</link><description>Recent content in Spec on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 10 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/spec/index.xml" rel="self" type="application/rss+xml"/><item><title>溢出 714px、22 個測試同時紅 — 單點修復與規範化的分界</title><link>https://tarrragon.github.io/blog/work-log/flutter_renderflex_overflow_prevention_spec/</link><pubDate>Fri, 10 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/work-log/flutter_renderflex_overflow_prevention_spec/</guid><description>&lt;blockquote>
&lt;p>&lt;strong>觸發場景&lt;/strong>：Flutter 書籍管理 App 的 UC-04 區塊測試，widget 測試 9/31 通過——22 個失敗全是同一種錯：&lt;code>RenderFlex overflowed by N pixels&lt;/code>。最大的一筆溢出 714px，來源是 &lt;code>advanced_search_widget.dart:106&lt;/code> 一個 Row 裡未受 &lt;code>Flexible&lt;/code> 包裹的 &lt;code>SegmentedButton&lt;/code>
&lt;strong>疑問來源&lt;/strong>：22 個同型失敗，是修 22 次、還是做一件別的事？
&lt;strong>整理目的&lt;/strong>：記下 overflow 的約束機制、測試環境尺寸的角色、以及「單點修復 vs 抽規範」的判準；附 stale ticket 接手的考古教訓
&lt;strong>本文邊界&lt;/strong>：素材是該專案 v0.31.1 的 W1-011 系列記錄（從發現、拆票、規範建立到修復完成、橫跨兩個多月）&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="機制row-不會替固定尺寸的子元件求情">機制：Row 不會替固定尺寸的子元件求情&lt;/h2>
&lt;p>&lt;code>RenderFlex overflowed&lt;/code> 的成因用一句話講完：&lt;strong>flex 容器裡的子元件宣告了固定尺寸需求、而容器的約束裝不下&lt;/strong>。Row 對子元件的預設處理是「你要多寬給多寬」，&lt;code>SegmentedButton&lt;/code> 這類內容驅動寬度的元件在窄約束下要求超過可用寬度時，Row 不會自動壓縮它——溢出、畫黃黑條、測試紅。&lt;/p>
&lt;p>修法的方向有三個位階：包 &lt;code>Flexible&lt;/code> / &lt;code>Expanded&lt;/code>（讓子元件接受壓縮）、換可捲動容器（內容本來就可能超過一屏）、重設計版面（內容密度本身不合理）。這次選的是第一種的變體——Wrap 方案，因為測試對 &lt;code>SegmentedButton&lt;/code> 的行為契約有明確要求、元件本身不能換。溢出量還有一個可判讀的性質：它是&lt;strong>內容需求與可用空間的差&lt;/strong>，714px 的溢出說明這不是差幾個 padding 的微調問題、是整段版面對窄螢幕沒有任何彈性策略。&lt;/p>
&lt;h2 id="測試環境的小尺寸是-feature">測試環境的小尺寸是 feature&lt;/h2>
&lt;p>22 個失敗集中在測試環境（800x600、以及另一批 375x812）現形，實機大螢幕上未必看得到——這容易被誤讀成「測試環境太苛刻」。方向要反過來：&lt;strong>測試環境的小尺寸是免費的窄螢幕模擬&lt;/strong>。真實使用者裡有小手機、有分割畫面、有字體放大（同專案另一批 &lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">Dialog 溢位隨狀態增長&lt;/a>的數據就是這樣量出來的），widget 測試的固定小尺寸把這些情境提前到 CI 裡。把測試尺寸調大讓紅燈消失，是把免費的檢查關掉。&lt;/p>
&lt;h2 id="判準同型失敗的數量決定產出的形態">判準：同型失敗的數量決定產出的形態&lt;/h2>
&lt;p>這次事件最值得記的是處置的形態。22 個同型失敗沒有變成 22 張修復票，而是先拆出一張分析票、產出一份 396 行的規範文件（&lt;code>ui-layout-overflow-prevention.md&lt;/code>）：六大 overflow 反模式、修法決策樹、Widget 測試檢查清單、既有元件與間距常數的對照表——然後修復票&lt;strong>依規範&lt;/strong>執行。&lt;/p>
&lt;p>判準跟 &lt;a href="https://tarrragon.github.io/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">#42 兩次門檻&lt;/a>同源、但這裡數量直接跳過了門檻爭論：同型失敗兩位數，說明這是&lt;strong>團隊寫版面的系統性慣性&lt;/strong>、不是某一行的手滑。單點修復對慣性無效——修完這 22 個、下一批新 widget 還會照舊寫。規範化的產出讓三件事變可能：修復者有決策樹可依（不用每處重新發明修法）、新程式碼有檢查清單可對、review 有反模式清單可引。成本結構跟&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">測試分診&lt;/a>的 ROI 排序一致：一次規範的固定成本、攤提給之後每一個版面。&lt;/p>
&lt;h2 id="附帶教訓stale-ticket-先考古再執行">附帶教訓：stale ticket 先考古、再執行&lt;/h2>
&lt;p>這張票還留了一筆流程教訓。W1-011 停滯 58 天後被接手，執行前的考古驗證發現多處漂移：記錄裡的程式碼路徑寫反（&lt;code>search/widgets&lt;/code> 實際是 &lt;code>widgets/search&lt;/code>）、失敗測試數寫 22 實際 23、5W1H 欄位不完整。&lt;strong>陳舊 ticket 的內文是它建立當下的快照&lt;/strong>，兩個月的 codebase 演化足以讓路徑、數量、甚至問題本身漂移——照著舊內文直接動手，會修錯位置或漏修新增的失敗。接手的正確順序是先重驗每一個事實聲明（重跑測試、重 grep 路徑）、更新票面、再執行——跟&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_migration_read_path_gap_fake_green/" data-link-title="遷移計畫有寫入、有消費、缺讀出 — read-path 缺口與 fixture 假綠" data-link-desc="資料模型遷移的通路要三段齊：寫入 backfill、讀取路徑、消費端 API。缺讀出那段時，新 API 拿到的永遠是空集合——而消費端測試的 fixture 自己建物件、不走真實讀取路徑，測試全綠掩蓋 runtime 靜默失效。依賴圖只列「誰先做」不列語意前提時，dashboard 的 ready 是假訊號。">read-path 分析&lt;/a>的「獨立重驗、勿盲信」是同一條紀律在時間軸上的版本。&lt;/p>
&lt;h2 id="判讀徵兆">判讀徵兆&lt;/h2>
&lt;ul>
&lt;li>widget 測試出現 &lt;code>RenderFlex overflowed&lt;/code>——先看 flex 容器裡哪個子元件沒有彈性策略（&lt;code>Flexible&lt;/code> / &lt;code>Expanded&lt;/code> / 可捲動），不是先調測試螢幕尺寸&lt;/li>
&lt;li>溢出量大（數百 px）——版面對窄約束沒有任何策略、需要結構性修法；溢出量小（個位數）——邊距層級的微調&lt;/li>
&lt;li>同型失敗兩位數——停止逐個修，先抽反模式與決策樹、讓修復與未來的新程式碼有同一份依據&lt;/li>
&lt;li>接手停滯超過數週的 ticket——內文的每個事實聲明（路徑、數量、現象）先重驗再引用&lt;/li>
&lt;/ul>
&lt;h2 id="相關閱讀">相關閱讀&lt;/h2>
&lt;ul>
&lt;li>同族數據：&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">16 個失敗只有 2 個是缺口&lt;/a>——那批的溢位類（idle 81px → error 167px 隨狀態增長）與本文合成 overflow 的兩個現場&lt;/li>
&lt;li>「單次修復 vs 制度化」的原則層：&lt;a href="https://tarrragon.github.io/blog/report/lint-scope-must-be-explicit-fact/" data-link-title="檢查規則的作用域要顯式列舉：零 error 可能是沒被檢查" data-link-desc="新增與既有受檢目錄同類的內容目錄時、或工具鏈長期零 error 卻累積出違規時使用。規則的作用域由路徑常數決定、該常數常同時被多個檢查共用，擴作用域會連帶擴語意；作用域是獨立於規則內容的 fact，驗收方式是先確認新規則對已知違規報錯。">#221 檢查規則的作用域要顯式列舉&lt;/a>引用的教訓同構——單張 ticket 裡的觀察不升格、下一個執行者不會讀到；本文的規範文件就是升格的形態&lt;/li>
&lt;li>概念地基：Flutter 的約束傳遞模型——&lt;a href="https://tarrragon.github.io/blog/work-log/flutter_hit_test_behavior/" data-link-title="Flutter HitTestBehavior：控制點擊命中測試的三種模式" data-link-desc="GestureDetector 點空白 padding 區沒反應、或點擊穿透/阻擋行為不符預期。HitTestBehavior 各模式（deferToChild / opaque / translucent）的命中規則與適用場景。">HitTestBehavior 三種模式&lt;/a>同屬「框架的隱式規則要顯式理解」家族&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<blockquote>
<p><strong>觸發場景</strong>：Flutter 書籍管理 App 的 UC-04 區塊測試，widget 測試 9/31 通過——22 個失敗全是同一種錯：<code>RenderFlex overflowed by N pixels</code>。最大的一筆溢出 714px，來源是 <code>advanced_search_widget.dart:106</code> 一個 Row 裡未受 <code>Flexible</code> 包裹的 <code>SegmentedButton</code>
<strong>疑問來源</strong>：22 個同型失敗，是修 22 次、還是做一件別的事？
<strong>整理目的</strong>：記下 overflow 的約束機制、測試環境尺寸的角色、以及「單點修復 vs 抽規範」的判準；附 stale ticket 接手的考古教訓
<strong>本文邊界</strong>：素材是該專案 v0.31.1 的 W1-011 系列記錄（從發現、拆票、規範建立到修復完成、橫跨兩個多月）</p></blockquote>
<hr>
<h2 id="機制row-不會替固定尺寸的子元件求情">機制：Row 不會替固定尺寸的子元件求情</h2>
<p><code>RenderFlex overflowed</code> 的成因用一句話講完：<strong>flex 容器裡的子元件宣告了固定尺寸需求、而容器的約束裝不下</strong>。Row 對子元件的預設處理是「你要多寬給多寬」，<code>SegmentedButton</code> 這類內容驅動寬度的元件在窄約束下要求超過可用寬度時，Row 不會自動壓縮它——溢出、畫黃黑條、測試紅。</p>
<p>修法的方向有三個位階：包 <code>Flexible</code> / <code>Expanded</code>（讓子元件接受壓縮）、換可捲動容器（內容本來就可能超過一屏）、重設計版面（內容密度本身不合理）。這次選的是第一種的變體——Wrap 方案，因為測試對 <code>SegmentedButton</code> 的行為契約有明確要求、元件本身不能換。溢出量還有一個可判讀的性質：它是<strong>內容需求與可用空間的差</strong>，714px 的溢出說明這不是差幾個 padding 的微調問題、是整段版面對窄螢幕沒有任何彈性策略。</p>
<h2 id="測試環境的小尺寸是-feature">測試環境的小尺寸是 feature</h2>
<p>22 個失敗集中在測試環境（800x600、以及另一批 375x812）現形，實機大螢幕上未必看得到——這容易被誤讀成「測試環境太苛刻」。方向要反過來：<strong>測試環境的小尺寸是免費的窄螢幕模擬</strong>。真實使用者裡有小手機、有分割畫面、有字體放大（同專案另一批 <a href="/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">Dialog 溢位隨狀態增長</a>的數據就是這樣量出來的），widget 測試的固定小尺寸把這些情境提前到 CI 裡。把測試尺寸調大讓紅燈消失，是把免費的檢查關掉。</p>
<h2 id="判準同型失敗的數量決定產出的形態">判準：同型失敗的數量決定產出的形態</h2>
<p>這次事件最值得記的是處置的形態。22 個同型失敗沒有變成 22 張修復票，而是先拆出一張分析票、產出一份 396 行的規範文件（<code>ui-layout-overflow-prevention.md</code>）：六大 overflow 反模式、修法決策樹、Widget 測試檢查清單、既有元件與間距常數的對照表——然後修復票<strong>依規範</strong>執行。</p>
<p>判準跟 <a href="/blog/report/two-occurrence-threshold/" data-link-title="2 次門檻：第一次是運氣、第二次是訊號" data-link-desc="同一個問題出現第 2 次時、就該停下來把處理層級升一階 — 從推理升到量測、從手動驗證升到自動化、從同方向嘗試升到換思路。第 1 次失敗的資訊不足、第 2 次提供「重複出現」的證據、值得付出升級成本。本文是 #11 / #15 / #20 / #23 四篇實作的共同抽象。">#42 兩次門檻</a>同源、但這裡數量直接跳過了門檻爭論：同型失敗兩位數，說明這是<strong>團隊寫版面的系統性慣性</strong>、不是某一行的手滑。單點修復對慣性無效——修完這 22 個、下一批新 widget 還會照舊寫。規範化的產出讓三件事變可能：修復者有決策樹可依（不用每處重新發明修法）、新程式碼有檢查清單可對、review 有反模式清單可引。成本結構跟<a href="/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">測試分診</a>的 ROI 排序一致：一次規範的固定成本、攤提給之後每一個版面。</p>
<h2 id="附帶教訓stale-ticket-先考古再執行">附帶教訓：stale ticket 先考古、再執行</h2>
<p>這張票還留了一筆流程教訓。W1-011 停滯 58 天後被接手，執行前的考古驗證發現多處漂移：記錄裡的程式碼路徑寫反（<code>search/widgets</code> 實際是 <code>widgets/search</code>）、失敗測試數寫 22 實際 23、5W1H 欄位不完整。<strong>陳舊 ticket 的內文是它建立當下的快照</strong>，兩個月的 codebase 演化足以讓路徑、數量、甚至問題本身漂移——照著舊內文直接動手，會修錯位置或漏修新增的失敗。接手的正確順序是先重驗每一個事實聲明（重跑測試、重 grep 路徑）、更新票面、再執行——跟<a href="/blog/work-log/flutter_migration_read_path_gap_fake_green/" data-link-title="遷移計畫有寫入、有消費、缺讀出 — read-path 缺口與 fixture 假綠" data-link-desc="資料模型遷移的通路要三段齊：寫入 backfill、讀取路徑、消費端 API。缺讀出那段時，新 API 拿到的永遠是空集合——而消費端測試的 fixture 自己建物件、不走真實讀取路徑，測試全綠掩蓋 runtime 靜默失效。依賴圖只列「誰先做」不列語意前提時，dashboard 的 ready 是假訊號。">read-path 分析</a>的「獨立重驗、勿盲信」是同一條紀律在時間軸上的版本。</p>
<h2 id="判讀徵兆">判讀徵兆</h2>
<ul>
<li>widget 測試出現 <code>RenderFlex overflowed</code>——先看 flex 容器裡哪個子元件沒有彈性策略（<code>Flexible</code> / <code>Expanded</code> / 可捲動），不是先調測試螢幕尺寸</li>
<li>溢出量大（數百 px）——版面對窄約束沒有任何策略、需要結構性修法；溢出量小（個位數）——邊距層級的微調</li>
<li>同型失敗兩位數——停止逐個修，先抽反模式與決策樹、讓修復與未來的新程式碼有同一份依據</li>
<li>接手停滯超過數週的 ticket——內文的每個事實聲明（路徑、數量、現象）先重驗再引用</li>
</ul>
<h2 id="相關閱讀">相關閱讀</h2>
<ul>
<li>同族數據：<a href="/blog/work-log/flutter_test_failure_triage_root_cause_roi/" data-link-title="16 個失敗只有 2 個是缺口 — 大規模測試失敗先分診、再按 ROI 修" data-link-desc="測試失敗數超過十個時逐個修是錯的順序：先全數分類根因、再按「單位工時救回的測試數」排修復順序。兩批實戰分類顯示半數失敗是斷言過時而非 bug、九個失敗共用一個 helper 修法；每類的症狀特徵字串可以建成索引讓下批失敗直接對號。">16 個失敗只有 2 個是缺口</a>——那批的溢位類（idle 81px → error 167px 隨狀態增長）與本文合成 overflow 的兩個現場</li>
<li>「單次修復 vs 制度化」的原則層：<a href="/blog/report/lint-scope-must-be-explicit-fact/" data-link-title="檢查規則的作用域要顯式列舉：零 error 可能是沒被檢查" data-link-desc="新增與既有受檢目錄同類的內容目錄時、或工具鏈長期零 error 卻累積出違規時使用。規則的作用域由路徑常數決定、該常數常同時被多個檢查共用，擴作用域會連帶擴語意；作用域是獨立於規則內容的 fact，驗收方式是先確認新規則對已知違規報錯。">#221 檢查規則的作用域要顯式列舉</a>引用的教訓同構——單張 ticket 裡的觀察不升格、下一個執行者不會讀到；本文的規範文件就是升格的形態</li>
<li>概念地基：Flutter 的約束傳遞模型——<a href="/blog/work-log/flutter_hit_test_behavior/" data-link-title="Flutter HitTestBehavior：控制點擊命中測試的三種模式" data-link-desc="GestureDetector 點空白 padding 區沒反應、或點擊穿透/阻擋行為不符預期。HitTestBehavior 各模式（deferToChild / opaque / translucent）的命中規則與適用場景。">HitTestBehavior 三種模式</a>同屬「框架的隱式規則要顯式理解」家族</li>
</ul>
]]></content:encoded></item></channel></rss>