<?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>模組一：畫面狀態機設計 on Tarragon</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/</link><description>Recent content in 模組一：畫面狀態機設計 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/index.xml" rel="self" type="application/rss+xml"/><item><title>畫面狀態矩陣的定義與填寫方法</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣&lt;/a>是一張表格，每行代表一個畫面的一個狀態，每列描述該狀態的四個面向：顯示了什麼、使用者能做什麼操作、什麼條件進入這個狀態、怎麼離開。這張表格的目的是在實作前暴露導航缺口 — 退出路徑欄位為空代表使用者一旦進入就出不去。&lt;/p>
&lt;h2 id="四欄定義">四欄定義&lt;/h2>
&lt;h3 id="顯示">顯示&lt;/h3>
&lt;p>使用者看到的畫面元素。進度指示器、錯誤訊息、表單欄位、資料列表 — 任何視覺上呈現的內容。&lt;/p>
&lt;h3 id="可用操作">可用操作&lt;/h3>
&lt;p>使用者在這個狀態下能執行的動作。按鈕、手勢、鍵盤輸入、下拉選單選擇。重點是「能做什麼」，不是「看到什麼」。&lt;/p>
&lt;p>「顯示」和「可用操作」的區別在於互動性。顯示一段錯誤訊息和顯示一個重試按鈕都是「顯示」；按下重試按鈕觸發重新連線是「可用操作」。&lt;/p>
&lt;h3 id="進入條件">進入條件&lt;/h3>
&lt;p>什麼事件或動作讓畫面進入這個狀態。使用者操作（點擊按鈕）、系統事件（連線成功）、外部事件（推播通知）。&lt;/p>
&lt;h3 id="退出路徑">退出路徑&lt;/h3>
&lt;p>使用者如何離開這個狀態。返回上一頁、導航到另一個畫面、取消當前操作、完成流程後自動轉場。&lt;/p>
&lt;p>退出路徑是這張表格中最容易遺漏的欄位。開發者設計畫面時，注意力集中在「進來後看到什麼、能做什麼」，容易忽略「怎麼離開」。&lt;/p>
&lt;h2 id="填寫步驟">填寫步驟&lt;/h2>
&lt;h3 id="第一步列出畫面的所有狀態">第一步：列出畫面的所有狀態&lt;/h3>
&lt;p>從程式碼中的狀態管理機制取得狀態清單。Flutter 的 enum、React 的 state、Vue 的 reactive data — 任何控制畫面呈現的狀態變數。&lt;/p>
&lt;p>如果沒有明確的狀態 enum，從畫面的視覺變化反推：loading 時長什麼樣、成功時長什麼樣、失敗時長什麼樣。每種不同的視覺呈現就是一個狀態。&lt;/p>
&lt;h3 id="第二步每個狀態填四欄">第二步：每個狀態填四欄&lt;/h3>
&lt;p>逐一填寫每個狀態的顯示、可用操作、進入條件、退出路徑。填不出來的欄位先留空，留空本身就是發現。&lt;/p>
&lt;h3 id="第三步檢查退出路徑欄">第三步：檢查退出路徑欄&lt;/h3>
&lt;p>退出路徑為空的狀態是 UX 死胡同。使用者進入後無法靠自己的操作離開，只能殺掉 app 或等系統逾時。&lt;/p>
&lt;p>app_tunnel 的 Terminal 畫面有五個 enum 狀態（idle / connecting / connected / error / disconnected），每個狀態的退出路徑都是空的。使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1&lt;/a>）。10 分鐘填完這張表格就能發現全部五個缺口（本章合成，UF-3 Derive）。&lt;/p>
&lt;h3 id="第四步檢查操作欄">第四步：檢查操作欄&lt;/h3>
&lt;p>操作欄為空的狀態可能合理（loading 時使用者等待），也可能代表缺少互動設計。loading 狀態通常應該有「取消」操作，error 狀態通常應該有「重試」和「返回」。&lt;/p>
&lt;h2 id="填寫範例">填寫範例&lt;/h2>
&lt;p>以 app_tunnel Terminal 畫面為例，修復前的矩陣如下：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>顯示&lt;/th>
 &lt;th>可用操作&lt;/th>
 &lt;th>進入條件&lt;/th>
 &lt;th>退出路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>idle&lt;/td>
 &lt;td>空白（自動連線）&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>進入畫面&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>進度指示&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>idle 自動觸發&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機 + 工具列&lt;/td>
 &lt;td>打字、特殊鍵&lt;/td>
 &lt;td>WebSocket 連線成功&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>連線失敗&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>「連線中斷」+ 重連按鈕&lt;/td>
 &lt;td>重新連線&lt;/td>
 &lt;td>連線斷開&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>退出路徑欄全空 — 五個 UX 死胡同。修復後的矩陣應該每個狀態都有至少一條退出路徑：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態&lt;/th>
 &lt;th>顯示&lt;/th>
 &lt;th>可用操作&lt;/th>
 &lt;th>進入條件&lt;/th>
 &lt;th>退出路徑&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>idle&lt;/td>
 &lt;td>空白（自動連線）&lt;/td>
 &lt;td>取消&lt;/td>
 &lt;td>進入畫面&lt;/td>
 &lt;td>取消 → 返回首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connecting&lt;/td>
 &lt;td>進度指示 + back 按鈕&lt;/td>
 &lt;td>取消連線&lt;/td>
 &lt;td>idle 自動觸發&lt;/td>
 &lt;td>back → 返回首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>connected&lt;/td>
 &lt;td>終端機 + 工具列 + back&lt;/td>
 &lt;td>打字、特殊鍵、中斷連線&lt;/td>
 &lt;td>WebSocket 連線成功&lt;/td>
 &lt;td>back / disconnect → 首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>error&lt;/td>
 &lt;td>錯誤訊息 + 重連 + back&lt;/td>
 &lt;td>重新連線、返回&lt;/td>
 &lt;td>連線失敗&lt;/td>
 &lt;td>back → 返回首頁&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>disconnected&lt;/td>
 &lt;td>中斷訊息 + 重連 + back&lt;/td>
 &lt;td>重新連線、返回&lt;/td>
 &lt;td>連線斷開&lt;/td>
 &lt;td>back → 返回首頁&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="每個狀態至少一條退出路徑">每個狀態至少一條退出路徑&lt;/h2>
&lt;p>退出路徑是預設要求。即使是 connecting 這種過渡狀態，使用者也應該能取消 — 連線卡住時使用者需要能離開。iOS HIG 和 Material Design 對 modal 畫面都要求 dismiss 機制；畫面狀態矩陣的退出路徑欄是這個要求的具體檢查方式。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣</a>是一張表格，每行代表一個畫面的一個狀態，每列描述該狀態的四個面向：顯示了什麼、使用者能做什麼操作、什麼條件進入這個狀態、怎麼離開。這張表格的目的是在實作前暴露導航缺口 — 退出路徑欄位為空代表使用者一旦進入就出不去。</p>
<h2 id="四欄定義">四欄定義</h2>
<h3 id="顯示">顯示</h3>
<p>使用者看到的畫面元素。進度指示器、錯誤訊息、表單欄位、資料列表 — 任何視覺上呈現的內容。</p>
<h3 id="可用操作">可用操作</h3>
<p>使用者在這個狀態下能執行的動作。按鈕、手勢、鍵盤輸入、下拉選單選擇。重點是「能做什麼」，不是「看到什麼」。</p>
<p>「顯示」和「可用操作」的區別在於互動性。顯示一段錯誤訊息和顯示一個重試按鈕都是「顯示」；按下重試按鈕觸發重新連線是「可用操作」。</p>
<h3 id="進入條件">進入條件</h3>
<p>什麼事件或動作讓畫面進入這個狀態。使用者操作（點擊按鈕）、系統事件（連線成功）、外部事件（推播通知）。</p>
<h3 id="退出路徑">退出路徑</h3>
<p>使用者如何離開這個狀態。返回上一頁、導航到另一個畫面、取消當前操作、完成流程後自動轉場。</p>
<p>退出路徑是這張表格中最容易遺漏的欄位。開發者設計畫面時，注意力集中在「進來後看到什麼、能做什麼」，容易忽略「怎麼離開」。</p>
<h2 id="填寫步驟">填寫步驟</h2>
<h3 id="第一步列出畫面的所有狀態">第一步：列出畫面的所有狀態</h3>
<p>從程式碼中的狀態管理機制取得狀態清單。Flutter 的 enum、React 的 state、Vue 的 reactive data — 任何控制畫面呈現的狀態變數。</p>
<p>如果沒有明確的狀態 enum，從畫面的視覺變化反推：loading 時長什麼樣、成功時長什麼樣、失敗時長什麼樣。每種不同的視覺呈現就是一個狀態。</p>
<h3 id="第二步每個狀態填四欄">第二步：每個狀態填四欄</h3>
<p>逐一填寫每個狀態的顯示、可用操作、進入條件、退出路徑。填不出來的欄位先留空，留空本身就是發現。</p>
<h3 id="第三步檢查退出路徑欄">第三步：檢查退出路徑欄</h3>
<p>退出路徑為空的狀態是 UX 死胡同。使用者進入後無法靠自己的操作離開，只能殺掉 app 或等系統逾時。</p>
<p>app_tunnel 的 Terminal 畫面有五個 enum 狀態（idle / connecting / connected / error / disconnected），每個狀態的退出路徑都是空的。使用者從首頁點 Connect Terminal 進入後，無論處於哪個狀態都無法返回首頁（<a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1</a>）。10 分鐘填完這張表格就能發現全部五個缺口（本章合成，UF-3 Derive）。</p>
<h3 id="第四步檢查操作欄">第四步：檢查操作欄</h3>
<p>操作欄為空的狀態可能合理（loading 時使用者等待），也可能代表缺少互動設計。loading 狀態通常應該有「取消」操作，error 狀態通常應該有「重試」和「返回」。</p>
<h2 id="填寫範例">填寫範例</h2>
<p>以 app_tunnel Terminal 畫面為例，修復前的矩陣如下：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>進入條件</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動連線）</td>
          <td>無</td>
          <td>進入畫面</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>進度指示</td>
          <td>無</td>
          <td>idle 自動觸發</td>
          <td>無</td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機 + 工具列</td>
          <td>打字、特殊鍵</td>
          <td>WebSocket 連線成功</td>
          <td>無</td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連按鈕</td>
          <td>重新連線</td>
          <td>連線失敗</td>
          <td>無</td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>「連線中斷」+ 重連按鈕</td>
          <td>重新連線</td>
          <td>連線斷開</td>
          <td>無</td>
      </tr>
  </tbody>
</table>
<p>退出路徑欄全空 — 五個 UX 死胡同。修復後的矩陣應該每個狀態都有至少一條退出路徑：</p>
<table>
  <thead>
      <tr>
          <th>狀態</th>
          <th>顯示</th>
          <th>可用操作</th>
          <th>進入條件</th>
          <th>退出路徑</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idle</td>
          <td>空白（自動連線）</td>
          <td>取消</td>
          <td>進入畫面</td>
          <td>取消 → 返回首頁</td>
      </tr>
      <tr>
          <td>connecting</td>
          <td>進度指示 + back 按鈕</td>
          <td>取消連線</td>
          <td>idle 自動觸發</td>
          <td>back → 返回首頁</td>
      </tr>
      <tr>
          <td>connected</td>
          <td>終端機 + 工具列 + back</td>
          <td>打字、特殊鍵、中斷連線</td>
          <td>WebSocket 連線成功</td>
          <td>back / disconnect → 首頁</td>
      </tr>
      <tr>
          <td>error</td>
          <td>錯誤訊息 + 重連 + back</td>
          <td>重新連線、返回</td>
          <td>連線失敗</td>
          <td>back → 返回首頁</td>
      </tr>
      <tr>
          <td>disconnected</td>
          <td>中斷訊息 + 重連 + back</td>
          <td>重新連線、返回</td>
          <td>連線斷開</td>
          <td>back → 返回首頁</td>
      </tr>
  </tbody>
</table>
<h2 id="每個狀態至少一條退出路徑">每個狀態至少一條退出路徑</h2>
<p>退出路徑是預設要求。即使是 connecting 這種過渡狀態，使用者也應該能取消 — 連線卡住時使用者需要能離開。iOS HIG 和 Material Design 對 modal 畫面都要求 dismiss 機制；畫面狀態矩陣的退出路徑欄是這個要求的具體檢查方式。</p>
<p>退出路徑為空只在一種情況下合理：該狀態持續時間極短（&lt; 1 秒）且有保證的自動轉場。即使如此，仍建議保留取消操作 — 因為「極短」是在正常情況下的預期，異常情況（網路中斷、服務當機）可能讓這個狀態停留很久。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>從 BDD 操作盤點展開到狀態矩陣 → <a href="/blog/ux-design/01-screen-state-machine/bdd-to-state-matrix/" data-link-title="從 BDD 操作盤點展開到狀態矩陣" data-link-desc="五步驟把 BDD 操作盤點的「前端引導」展開成完整的畫面狀態矩陣 — 補上操作和退出這兩個容易遺漏的面向">從 BDD 操作盤點展開到狀態矩陣</a></li>
<li>路由可達性檢查 → <a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性檢查</a></li>
<li>狀態矩陣轉 widget test case → <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化</a></li>
<li>狀態矩陣加「可觀測性」欄位 → <a href="/blog/testing/02-client-observability/" data-link-title="模組二：客戶端可觀測性" data-link-desc="連線生命週期 log、protocol 訊息 log、使用者行為 log — log 設計是功能規格的一部分">testing 模組二 客戶端可觀測性</a></li>
<li>狀態轉換事件作為 funnel 分析原料 → <a href="/blog/monitoring/08-business-analytics/" data-link-title="模組八：行為資料的商業利用" data-link-desc="Funnel / Cohort / Attribution / A/B test / 推薦系統 / RFM — 從 debug 工具到商業資產的翻轉">monitoring 模組八 行為資料的商業利用</a></li>
</ul>
]]></content:encoded></item><item><title>從 BDD 操作盤點展開到狀態矩陣</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/bdd-to-state-matrix/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/bdd-to-state-matrix/</guid><description>&lt;p>BDD 操作盤點描述使用者操作的情境和預期結果，但操作盤點的格式（Given / When / Then）聚焦在「什麼情境下做什麼得到什麼」，容易漏掉畫面層級的兩個面向：每個狀態下使用者能執行哪些操作，以及如何離開當前狀態。&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣&lt;/a>補上這兩個面向，讓導航缺口在實作前浮現。&lt;/p>
&lt;h2 id="操作盤點的覆蓋範圍">操作盤點的覆蓋範圍&lt;/h2>
&lt;p>BDD 操作盤點通常包含：&lt;/p>
&lt;ul>
&lt;li>使用者操作（When）：「使用者點擊連線按鈕」&lt;/li>
&lt;li>前端引導（Then）：「顯示連線進度指示」&lt;/li>
&lt;li>後端回應：「WebSocket 連線建立」&lt;/li>
&lt;/ul>
&lt;p>「前端引導」描述的是畫面的顯示內容 — 對應狀態矩陣的「顯示」欄。但操作盤點通常不會展開：連線中的畫面除了顯示進度指示，使用者能做什麼？如果連線失敗，使用者怎麼離開失敗畫面？&lt;/p>
&lt;p>app_tunnel 的操作盤點在「前端引導」欄寫了「連線失敗顯示無法連線」，覆蓋了 error 狀態的顯示。但是「顯示無法連線之後使用者能做什麼」和「使用者怎麼離開這個畫面」都沒有描述。實作出來的 error 狀態有重連按鈕但沒有 back 按鈕 — 重連失敗時使用者被困在 error → retry → error 循環裡（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1&lt;/a>）。&lt;/p>
&lt;h2 id="展開步驟">展開步驟&lt;/h2>
&lt;h3 id="步驟一從操作盤點抽出所有畫面">步驟一：從操作盤點抽出所有畫面&lt;/h3>
&lt;p>每個 BDD 情境至少涉及一個畫面。列出所有情境提到的畫面名稱，去重後得到畫面清單。&lt;/p>
&lt;p>例：app_tunnel 的操作盤點涉及三個畫面 — 首頁、配對畫面、終端機畫面。&lt;/p>
&lt;h3 id="步驟二每個畫面列出狀態">步驟二：每個畫面列出狀態&lt;/h3>
&lt;p>從操作盤點的 Given / When / Then 條件中抽出狀態。「Given 連線已建立」暗示有 connected 狀態；「Then 顯示無法連線」暗示有 error 狀態。&lt;/p>
&lt;p>同時檢查程式碼中的狀態 enum — 操作盤點可能遺漏了某些狀態（如 idle、disconnected），程式碼裡有但操作盤點沒提到的狀態同樣需要設計 UI。&lt;/p>
&lt;h3 id="步驟三每個狀態填顯示欄">步驟三：每個狀態填「顯示」欄&lt;/h3>
&lt;p>從操作盤點的「前端引導」直接填入。這一步通常不缺資料，因為操作盤點的強項就是描述顯示內容。&lt;/p>
&lt;h3 id="步驟四每個狀態填可用操作和退出路徑欄">步驟四：每個狀態填「可用操作」和「退出路徑」欄&lt;/h3>
&lt;p>這一步是關鍵 — 操作盤點通常不提供這些資訊，需要主動補上。&lt;/p>
&lt;p>對每個狀態問兩個問題：&lt;/p>
&lt;ul>
&lt;li>使用者在這個狀態下想做什麼？（可用操作）&lt;/li>
&lt;li>使用者怎麼離開這個狀態？（退出路徑）&lt;/li>
&lt;/ul>
&lt;p>開發者容易假設 connected 狀態下使用者只想打字，不會想返回首頁。但使用者可能想切換到配對畫面重新配對、想暫時離開做其他事、想結束當前操作。把這些可能性列出來，判斷哪些需要提供操作按鈕。&lt;/p>
&lt;h3 id="步驟五檢查矩陣的空白格">步驟五：檢查矩陣的空白格&lt;/h3>
&lt;p>退出路徑欄為空的狀態是 UX 死胡同，需要補上退出路徑。可用操作欄為空的狀態需要判斷是否合理 — loading 狀態操作欄為空可能合理，但建議至少提供取消操作。&lt;/p>
&lt;h2 id="操作盤點的描述顯示偏差">操作盤點的「描述顯示」偏差&lt;/h2>
&lt;p>操作盤點的「前端引導」傾向描述顯示（What the user sees）而非描述互動（What the user can do）。這個偏差的根源在 BDD 的 Then 語法 — Then 通常描述可觀察的結果，而「畫面顯示 X」比「使用者可以做 Y」更容易寫成可觀察的斷言。&lt;/p>
&lt;p>app_tunnel 的操作盤點就是這個模式。四個操作情境的「前端引導」都寫了顯示內容（「顯示終端機畫面」「顯示連線中」「顯示無法連線」），沒有一個寫了操作（「使用者可以取消」「使用者可以返回」）或退出路徑（「使用者可以回到首頁」）（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1&lt;/a>）。&lt;/p>
&lt;p>畫面狀態矩陣的四欄結構強制補上這兩個面向。從 BDD 操作盤點到畫面狀態矩陣的展開步驟，就是把「只描述顯示」擴展成「顯示 + 操作 + 退出」的過程。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>畫面狀態矩陣的完整定義 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>路由可達性檢查 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性檢查&lt;/a>&lt;/li>
&lt;li>想知道什麼是「假設只走 happy path」的反模式 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/anti-pattern-happy-path-only/" data-link-title="反模式：假設使用者只走 happy path" data-link-desc="為什麼開發者容易只設計 happy path 的 UI、使用者在非 happy path 狀態下被困住的機制分析、以及用狀態矩陣系統性地防止這個問題">反模式：假設使用者只走 happy path&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>BDD 操作盤點描述使用者操作的情境和預期結果，但操作盤點的格式（Given / When / Then）聚焦在「什麼情境下做什麼得到什麼」，容易漏掉畫面層級的兩個面向：每個狀態下使用者能執行哪些操作，以及如何離開當前狀態。<a href="/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣</a>補上這兩個面向，讓導航缺口在實作前浮現。</p>
<h2 id="操作盤點的覆蓋範圍">操作盤點的覆蓋範圍</h2>
<p>BDD 操作盤點通常包含：</p>
<ul>
<li>使用者操作（When）：「使用者點擊連線按鈕」</li>
<li>前端引導（Then）：「顯示連線進度指示」</li>
<li>後端回應：「WebSocket 連線建立」</li>
</ul>
<p>「前端引導」描述的是畫面的顯示內容 — 對應狀態矩陣的「顯示」欄。但操作盤點通常不會展開：連線中的畫面除了顯示進度指示，使用者能做什麼？如果連線失敗，使用者怎麼離開失敗畫面？</p>
<p>app_tunnel 的操作盤點在「前端引導」欄寫了「連線失敗顯示無法連線」，覆蓋了 error 狀態的顯示。但是「顯示無法連線之後使用者能做什麼」和「使用者怎麼離開這個畫面」都沒有描述。實作出來的 error 狀態有重連按鈕但沒有 back 按鈕 — 重連失敗時使用者被困在 error → retry → error 循環裡（<a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1</a>）。</p>
<h2 id="展開步驟">展開步驟</h2>
<h3 id="步驟一從操作盤點抽出所有畫面">步驟一：從操作盤點抽出所有畫面</h3>
<p>每個 BDD 情境至少涉及一個畫面。列出所有情境提到的畫面名稱，去重後得到畫面清單。</p>
<p>例：app_tunnel 的操作盤點涉及三個畫面 — 首頁、配對畫面、終端機畫面。</p>
<h3 id="步驟二每個畫面列出狀態">步驟二：每個畫面列出狀態</h3>
<p>從操作盤點的 Given / When / Then 條件中抽出狀態。「Given 連線已建立」暗示有 connected 狀態；「Then 顯示無法連線」暗示有 error 狀態。</p>
<p>同時檢查程式碼中的狀態 enum — 操作盤點可能遺漏了某些狀態（如 idle、disconnected），程式碼裡有但操作盤點沒提到的狀態同樣需要設計 UI。</p>
<h3 id="步驟三每個狀態填顯示欄">步驟三：每個狀態填「顯示」欄</h3>
<p>從操作盤點的「前端引導」直接填入。這一步通常不缺資料，因為操作盤點的強項就是描述顯示內容。</p>
<h3 id="步驟四每個狀態填可用操作和退出路徑欄">步驟四：每個狀態填「可用操作」和「退出路徑」欄</h3>
<p>這一步是關鍵 — 操作盤點通常不提供這些資訊，需要主動補上。</p>
<p>對每個狀態問兩個問題：</p>
<ul>
<li>使用者在這個狀態下想做什麼？（可用操作）</li>
<li>使用者怎麼離開這個狀態？（退出路徑）</li>
</ul>
<p>開發者容易假設 connected 狀態下使用者只想打字，不會想返回首頁。但使用者可能想切換到配對畫面重新配對、想暫時離開做其他事、想結束當前操作。把這些可能性列出來，判斷哪些需要提供操作按鈕。</p>
<h3 id="步驟五檢查矩陣的空白格">步驟五：檢查矩陣的空白格</h3>
<p>退出路徑欄為空的狀態是 UX 死胡同，需要補上退出路徑。可用操作欄為空的狀態需要判斷是否合理 — loading 狀態操作欄為空可能合理，但建議至少提供取消操作。</p>
<h2 id="操作盤點的描述顯示偏差">操作盤點的「描述顯示」偏差</h2>
<p>操作盤點的「前端引導」傾向描述顯示（What the user sees）而非描述互動（What the user can do）。這個偏差的根源在 BDD 的 Then 語法 — Then 通常描述可觀察的結果，而「畫面顯示 X」比「使用者可以做 Y」更容易寫成可觀察的斷言。</p>
<p>app_tunnel 的操作盤點就是這個模式。四個操作情境的「前端引導」都寫了顯示內容（「顯示終端機畫面」「顯示連線中」「顯示無法連線」），沒有一個寫了操作（「使用者可以取消」「使用者可以返回」）或退出路徑（「使用者可以回到首頁」）（<a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1</a>）。</p>
<p>畫面狀態矩陣的四欄結構強制補上這兩個面向。從 BDD 操作盤點到畫面狀態矩陣的展開步驟，就是把「只描述顯示」擴展成「顯示 + 操作 + 退出」的過程。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>畫面狀態矩陣的完整定義 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>路由可達性檢查 → <a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性檢查</a></li>
<li>想知道什麼是「假設只走 happy path」的反模式 → <a href="/blog/ux-design/01-screen-state-machine/anti-pattern-happy-path-only/" data-link-title="反模式：假設使用者只走 happy path" data-link-desc="為什麼開發者容易只設計 happy path 的 UI、使用者在非 happy path 狀態下被困住的機制分析、以及用狀態矩陣系統性地防止這個問題">反模式：假設使用者只走 happy path</a></li>
</ul>
]]></content:encoded></item><item><title>路由可達性檢查</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/</guid><description>&lt;p>路由可達性檢查比較兩個集合：router 定義的所有路由，和使用者從 UI 操作能到達的所有路由。兩個集合的差集就是問題所在 — 定義了但不可達的路由是入口缺失，可達但未定義的路由是 404 風險。&lt;/p>
&lt;h2 id="定義-vs-可達">定義 vs 可達&lt;/h2>
&lt;h3 id="router-定義的路由">Router 定義的路由&lt;/h3>
&lt;p>現代前端框架（Flutter GoRouter、React Router、Vue Router）通常有一個集中的路由定義檔，列出所有可存取的路徑和對應的畫面元件。這個列表是 router 認知的「所有畫面」。&lt;/p>
&lt;h3 id="ui-可達的路由">UI 可達的路由&lt;/h3>
&lt;p>從首頁（或 app 的入口畫面）開始，透過 UI 上的按鈕、連結、手勢能到達的所有路由。這個集合代表使用者實際能存取的畫面。&lt;/p>
&lt;h3 id="差集分析">差集分析&lt;/h3>
&lt;p>&lt;strong>router 有但 UI 不可達&lt;/strong>：路由定義了、畫面元件也實作了，但沒有任何 UI 元素導航到這個路由。功能存在但使用者找不到入口。&lt;/p>
&lt;p>&lt;strong>UI 指向但 router 沒有&lt;/strong>：UI 上有一個按鈕 &lt;code>navigateTo('/settings')&lt;/code>，但 router 沒有定義 &lt;code>/settings&lt;/code> 路由。使用者點擊後會看到 404 或空白畫面。&lt;/p>
&lt;h2 id="路由存在但不可達的案例">路由存在但不可達的案例&lt;/h2>
&lt;p>app_tunnel 的 router 定義了三條路由：&lt;code>/&lt;/code>（首頁）、&lt;code>/enrollment&lt;/code>（配對）、&lt;code>/terminal&lt;/code>（終端機）。首頁只有一個 Connect Terminal 按鈕導航到 &lt;code>/terminal&lt;/code>。&lt;code>/enrollment&lt;/code> 路由存在，&lt;code>EnrollmentScreen&lt;/code> 完整實作，但首頁沒有任何 UI 元素導航到這個路由（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>）。&lt;/p>
&lt;p>從使用者視角看，配對功能不存在。從開發者視角看，配對功能完整 — 路由定義了、畫面寫好了、業務邏輯都通了。問題出在「入口」這個連接層。&lt;/p>
&lt;p>這和程式碼裡寫了一個 function 但沒有任何地方呼叫它的情況結構相同。Function 本身可能正確無誤，但從系統角度看是死程式碼。路由可達性檢查是這個問題在 UX 層的對應。&lt;/p>
&lt;h2 id="檢查方法">檢查方法&lt;/h2>
&lt;h3 id="手動檢查">手動檢查&lt;/h3>
&lt;p>列出 router 定義的所有路由，然後逐一在 UI 上找到通往該路由的操作路徑。找不到路徑的就是不可達路由。&lt;/p>
&lt;p>手動檢查的成本隨畫面數量線性增長。5 個路由的 app 很快能查完；50 個路由的 app 需要系統化方法。&lt;/p>
&lt;h3 id="從操作盤點交叉比對">從操作盤點交叉比對&lt;/h3>
&lt;p>BDD 操作盤點列出了所有使用者操作（UC）。每個 UC 對應至少一個畫面。把 UC 清單和 router 定義對照：&lt;/p>
&lt;ul>
&lt;li>每個 UC 的主要入口畫面是否有從首頁可達的路徑？&lt;/li>
&lt;li>每個 UC 涉及的中間畫面是否都有進入和退出路徑？&lt;/li>
&lt;/ul>
&lt;p>app_tunnel 的操作盤點列了四個操作（配對、連線、輪替、啟停），首頁只提供了「連線」的入口。「配對」是 app 操作，應該有入口但沒有。「輪替」和「啟停」是主機端操作，不需要 app 入口。這個交叉比對能在 5 分鐘內揭露入口缺失。&lt;/p>
&lt;h3 id="自動化檢查">自動化檢查&lt;/h3>
&lt;p>從 router 定義檔解析所有路由路徑，再從 UI 元件的程式碼中搜尋所有 &lt;code>navigateTo&lt;/code>、&lt;code>context.go&lt;/code>、&lt;code>context.push&lt;/code>、&lt;code>router.push&lt;/code> 等導航呼叫的目標路徑。兩個集合取差集。&lt;/p>
&lt;p>自動化檢查能發現靜態定義的入口缺失，但無法發現動態導航（根據執行期條件決定目標路由）的可達性問題。&lt;/p>
&lt;h2 id="go-vs-push-的語意影響">&lt;code>go&lt;/code> vs &lt;code>push&lt;/code> 的語意影響&lt;/h2>
&lt;p>路由可達性確認之後，導航方式的選擇影響使用者的返回路徑。&lt;/p>
&lt;p>&lt;code>push&lt;/code> 把新畫面推入導航堆疊，使用者按 back 能回到前一個畫面。&lt;code>go&lt;/code> 替換整個導航堆疊，使用者按 back 不會回到原來的畫面。&lt;/p>
&lt;p>選擇 &lt;code>go&lt;/code> 還是 &lt;code>push&lt;/code> 取決於使用者的心理模型：這個導航是「暫時離開主畫面去做一件事，做完回來」（push），還是「切換到另一個主要工作區」（go）。&lt;/p>
&lt;p>app_tunnel 修復時選擇 &lt;code>context.push('/enrollment')&lt;/code> 讓使用者配對完成後按 back 回首頁 — 配對是「暫時去做一件事」，不是切換工作區（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>畫面狀態矩陣完整定義 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>想測試導航路徑的正確性 → &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化&lt;/a>&lt;/li>
&lt;li>想設計完整導航模式 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>路由可達性檢查比較兩個集合：router 定義的所有路由，和使用者從 UI 操作能到達的所有路由。兩個集合的差集就是問題所在 — 定義了但不可達的路由是入口缺失，可達但未定義的路由是 404 風險。</p>
<h2 id="定義-vs-可達">定義 vs 可達</h2>
<h3 id="router-定義的路由">Router 定義的路由</h3>
<p>現代前端框架（Flutter GoRouter、React Router、Vue Router）通常有一個集中的路由定義檔，列出所有可存取的路徑和對應的畫面元件。這個列表是 router 認知的「所有畫面」。</p>
<h3 id="ui-可達的路由">UI 可達的路由</h3>
<p>從首頁（或 app 的入口畫面）開始，透過 UI 上的按鈕、連結、手勢能到達的所有路由。這個集合代表使用者實際能存取的畫面。</p>
<h3 id="差集分析">差集分析</h3>
<p><strong>router 有但 UI 不可達</strong>：路由定義了、畫面元件也實作了，但沒有任何 UI 元素導航到這個路由。功能存在但使用者找不到入口。</p>
<p><strong>UI 指向但 router 沒有</strong>：UI 上有一個按鈕 <code>navigateTo('/settings')</code>，但 router 沒有定義 <code>/settings</code> 路由。使用者點擊後會看到 404 或空白畫面。</p>
<h2 id="路由存在但不可達的案例">路由存在但不可達的案例</h2>
<p>app_tunnel 的 router 定義了三條路由：<code>/</code>（首頁）、<code>/enrollment</code>（配對）、<code>/terminal</code>（終端機）。首頁只有一個 Connect Terminal 按鈕導航到 <code>/terminal</code>。<code>/enrollment</code> 路由存在，<code>EnrollmentScreen</code> 完整實作，但首頁沒有任何 UI 元素導航到這個路由（<a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a>）。</p>
<p>從使用者視角看，配對功能不存在。從開發者視角看，配對功能完整 — 路由定義了、畫面寫好了、業務邏輯都通了。問題出在「入口」這個連接層。</p>
<p>這和程式碼裡寫了一個 function 但沒有任何地方呼叫它的情況結構相同。Function 本身可能正確無誤，但從系統角度看是死程式碼。路由可達性檢查是這個問題在 UX 層的對應。</p>
<h2 id="檢查方法">檢查方法</h2>
<h3 id="手動檢查">手動檢查</h3>
<p>列出 router 定義的所有路由，然後逐一在 UI 上找到通往該路由的操作路徑。找不到路徑的就是不可達路由。</p>
<p>手動檢查的成本隨畫面數量線性增長。5 個路由的 app 很快能查完；50 個路由的 app 需要系統化方法。</p>
<h3 id="從操作盤點交叉比對">從操作盤點交叉比對</h3>
<p>BDD 操作盤點列出了所有使用者操作（UC）。每個 UC 對應至少一個畫面。把 UC 清單和 router 定義對照：</p>
<ul>
<li>每個 UC 的主要入口畫面是否有從首頁可達的路徑？</li>
<li>每個 UC 涉及的中間畫面是否都有進入和退出路徑？</li>
</ul>
<p>app_tunnel 的操作盤點列了四個操作（配對、連線、輪替、啟停），首頁只提供了「連線」的入口。「配對」是 app 操作，應該有入口但沒有。「輪替」和「啟停」是主機端操作，不需要 app 入口。這個交叉比對能在 5 分鐘內揭露入口缺失。</p>
<h3 id="自動化檢查">自動化檢查</h3>
<p>從 router 定義檔解析所有路由路徑，再從 UI 元件的程式碼中搜尋所有 <code>navigateTo</code>、<code>context.go</code>、<code>context.push</code>、<code>router.push</code> 等導航呼叫的目標路徑。兩個集合取差集。</p>
<p>自動化檢查能發現靜態定義的入口缺失，但無法發現動態導航（根據執行期條件決定目標路由）的可達性問題。</p>
<h2 id="go-vs-push-的語意影響"><code>go</code> vs <code>push</code> 的語意影響</h2>
<p>路由可達性確認之後，導航方式的選擇影響使用者的返回路徑。</p>
<p><code>push</code> 把新畫面推入導航堆疊，使用者按 back 能回到前一個畫面。<code>go</code> 替換整個導航堆疊，使用者按 back 不會回到原來的畫面。</p>
<p>選擇 <code>go</code> 還是 <code>push</code> 取決於使用者的心理模型：這個導航是「暫時離開主畫面去做一件事，做完回來」（push），還是「切換到另一個主要工作區」（go）。</p>
<p>app_tunnel 修復時選擇 <code>context.push('/enrollment')</code> 讓使用者配對完成後按 back 回首頁 — 配對是「暫時去做一件事」，不是切換工作區（<a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a>）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>畫面狀態矩陣完整定義 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>想測試導航路徑的正確性 → <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化</a></li>
<li>想設計完整導航模式 → <a href="/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式</a></li>
</ul>
]]></content:encoded></item><item><title>反模式：假設使用者只走 happy path</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/anti-pattern-happy-path-only/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/anti-pattern-happy-path-only/</guid><description>&lt;p>假設使用者只走 happy path 是指 UI 設計只覆蓋「一切順利」的情境 — 連線成功後打字、配對成功後連線、操作完成後返回 — 而忽略使用者在非順利情境下的需求。非順利情境包括：等待中想取消、失敗後想換方向、成功後想離開、中途想做其他事。&lt;/p>
&lt;h2 id="隱性假設如何產生">隱性假設如何產生&lt;/h2>
&lt;p>開發者設計 connected 狀態時，注意力在「終端機介面的功能」— 打字、特殊鍵、滾動。「使用者可能想離開 connected 狀態回到首頁」這個需求在 happy path 中不存在 — 如果一切順利，使用者為什麼要離開？&lt;/p>
&lt;p>這個推論在使用者行為和開發者假設吻合時成立。但使用者可能想：&lt;/p>
&lt;ul>
&lt;li>切換到配對畫面重新配對另一台裝置&lt;/li>
&lt;li>暫時離開終端機處理其他事&lt;/li>
&lt;li>遇到回應異常想從頭重新連線&lt;/li>
&lt;li>覺得功能不符需求想回到首頁看其他選項&lt;/li>
&lt;/ul>
&lt;p>app_tunnel 的 Terminal 畫面五個狀態都沒有退出路徑。connected 狀態有打字和特殊鍵操作，但沒有「離開」操作；error 和 disconnected 有重連按鈕，但沒有「放棄重連、回首頁」的選項。開發者設計 error 狀態時的隱性假設是「使用者遇到錯誤會想重試」— 沒考慮「使用者可能想放棄」（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1&lt;/a>）。&lt;/p>
&lt;h2 id="happy-path-偏差的擴散">Happy path 偏差的擴散&lt;/h2>
&lt;p>Happy path 偏差不只發生在單一畫面。首頁只放 Connect Terminal 按鈕、沒放配對入口，是首頁層級的 happy path 偏差 — 假設使用者已經完成配對、只需要連線（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>）。&lt;/p>
&lt;p>操作盤點的「前端引導」只描述顯示不描述操作和退出，是設計流程層級的 happy path 偏差 — 關注「順利時使用者看到什麼」，忽略「不順利時使用者能做什麼」。&lt;/p>
&lt;p>從企劃到設計到實作，每一層的 happy path 偏差累積起來，最終產出的 app 在正常情境下運作良好，在任何偏離的情境下使用者都被困住。&lt;/p>
&lt;h2 id="用狀態矩陣防止">用狀態矩陣防止&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣&lt;/a>的四欄結構（顯示 / 可用操作 / 進入條件 / 退出路徑）強制設計者回答每個狀態的操作和退出問題。不需要預測使用者的所有行為，只需要機械式地對每個狀態填寫四欄 — 空白格自動暴露缺口。&lt;/p>
&lt;p>具體的檢查規則：&lt;/p>
&lt;p>&lt;strong>退出路徑欄為空&lt;/strong>：需要補退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。&lt;/p>
&lt;p>&lt;strong>可用操作欄只有一個選項&lt;/strong>：使用者在這個狀態下只有一條路。如果這條路走不通（重連失敗），使用者被困住。至少考慮加一條替代路徑（返回、取消）。&lt;/p>
&lt;p>&lt;strong>進入條件是不可逆的&lt;/strong>：使用者無法自行觸發進入條件的反向操作（例如「連線成功」的反向操作「斷開連線」不存在）。這代表使用者進入後無法主動退出，只能等系統狀態變化。&lt;/p>
&lt;p>狀態矩陣的價值在於機械式的完整性檢查。填完所有狀態的四欄是一個 10 分鐘的工作（以 5 個狀態的畫面為例），產出是一張可以直接轉為 test case 的表格，同時能在實作前發現所有退出路徑缺口。&lt;/p>
&lt;p>矩陣的四欄定義和填寫步驟在&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>中完整展開。如果畫面設計從 BDD 操作盤點出發，&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/bdd-to-state-matrix/" data-link-title="從 BDD 操作盤點展開到狀態矩陣" data-link-desc="五步驟把 BDD 操作盤點的「前端引導」展開成完整的畫面狀態矩陣 — 補上操作和退出這兩個容易遺漏的面向">從 BDD 操作盤點展開到狀態矩陣&lt;/a>提供五步驟的展開流程。填完的矩陣可以直接轉成 widget test case — 具體方法見 &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>假設使用者只走 happy path 是指 UI 設計只覆蓋「一切順利」的情境 — 連線成功後打字、配對成功後連線、操作完成後返回 — 而忽略使用者在非順利情境下的需求。非順利情境包括：等待中想取消、失敗後想換方向、成功後想離開、中途想做其他事。</p>
<h2 id="隱性假設如何產生">隱性假設如何產生</h2>
<p>開發者設計 connected 狀態時，注意力在「終端機介面的功能」— 打字、特殊鍵、滾動。「使用者可能想離開 connected 狀態回到首頁」這個需求在 happy path 中不存在 — 如果一切順利，使用者為什麼要離開？</p>
<p>這個推論在使用者行為和開發者假設吻合時成立。但使用者可能想：</p>
<ul>
<li>切換到配對畫面重新配對另一台裝置</li>
<li>暫時離開終端機處理其他事</li>
<li>遇到回應異常想從頭重新連線</li>
<li>覺得功能不符需求想回到首頁看其他選項</li>
</ul>
<p>app_tunnel 的 Terminal 畫面五個狀態都沒有退出路徑。connected 狀態有打字和特殊鍵操作，但沒有「離開」操作；error 和 disconnected 有重連按鈕，但沒有「放棄重連、回首頁」的選項。開發者設計 error 狀態時的隱性假設是「使用者遇到錯誤會想重試」— 沒考慮「使用者可能想放棄」（<a href="/blog/ux-design/cases/five-states-zero-exits/" data-link-title="U.C1 Terminal 畫面五個狀態零個退出路徑" data-link-desc="Flutter app 的 Terminal 畫面有 idle/connecting/connected/error/disconnected 五個 enum 狀態，每個狀態都沒有 back 或 disconnect 按鈕 — 使用者一旦進入就出不去">U.C1</a>）。</p>
<h2 id="happy-path-偏差的擴散">Happy path 偏差的擴散</h2>
<p>Happy path 偏差不只發生在單一畫面。首頁只放 Connect Terminal 按鈕、沒放配對入口，是首頁層級的 happy path 偏差 — 假設使用者已經完成配對、只需要連線（<a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a>）。</p>
<p>操作盤點的「前端引導」只描述顯示不描述操作和退出，是設計流程層級的 happy path 偏差 — 關注「順利時使用者看到什麼」，忽略「不順利時使用者能做什麼」。</p>
<p>從企劃到設計到實作，每一層的 happy path 偏差累積起來，最終產出的 app 在正常情境下運作良好，在任何偏離的情境下使用者都被困住。</p>
<h2 id="用狀態矩陣防止">用狀態矩陣防止</h2>
<p><a href="/blog/ux-design/knowledge-cards/screen-state-matrix/" data-link-title="畫面狀態矩陣" data-link-desc="說明用四欄表格（顯示/可用操作/進入條件/退出路徑）系統性地暴露畫面導航缺口的設計工具">畫面狀態矩陣</a>的四欄結構（顯示 / 可用操作 / 進入條件 / 退出路徑）強制設計者回答每個狀態的操作和退出問題。不需要預測使用者的所有行為，只需要機械式地對每個狀態填寫四欄 — 空白格自動暴露缺口。</p>
<p>具體的檢查規則：</p>
<p><strong>退出路徑欄為空</strong>：需要補退出路徑。即使是 connecting 這種過渡狀態，使用者也應該能取消。</p>
<p><strong>可用操作欄只有一個選項</strong>：使用者在這個狀態下只有一條路。如果這條路走不通（重連失敗），使用者被困住。至少考慮加一條替代路徑（返回、取消）。</p>
<p><strong>進入條件是不可逆的</strong>：使用者無法自行觸發進入條件的反向操作（例如「連線成功」的反向操作「斷開連線」不存在）。這代表使用者進入後無法主動退出，只能等系統狀態變化。</p>
<p>狀態矩陣的價值在於機械式的完整性檢查。填完所有狀態的四欄是一個 10 分鐘的工作（以 5 個狀態的畫面為例），產出是一張可以直接轉為 test case 的表格，同時能在實作前發現所有退出路徑缺口。</p>
<p>矩陣的四欄定義和填寫步驟在<a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a>中完整展開。如果畫面設計從 BDD 操作盤點出發，<a href="/blog/ux-design/01-screen-state-machine/bdd-to-state-matrix/" data-link-title="從 BDD 操作盤點展開到狀態矩陣" data-link-desc="五步驟把 BDD 操作盤點的「前端引導」展開成完整的畫面狀態矩陣 — 補上操作和退出這兩個容易遺漏的面向">從 BDD 操作盤點展開到狀態矩陣</a>提供五步驟的展開流程。填完的矩陣可以直接轉成 widget test case — 具體方法見 <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化</a>。</p>
]]></content:encoded></item></channel></rss>