From b6e11990cc9bd3c838ad6cdd9e53caafff0fd7cb Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 18 Jun 2026 23:24:21 +0800
Subject: [PATCH] Add vertical App Shelf keyboard focus

---
 C3.tools/round1-keyboard-navigation-fixture-qa.sh |   63 +++++++++++++++++++++----------
 1 files changed, 43 insertions(+), 20 deletions(-)

diff --git a/C3.tools/round1-keyboard-navigation-fixture-qa.sh b/C3.tools/round1-keyboard-navigation-fixture-qa.sh
index 89eba81..fa10a3e 100755
--- a/C3.tools/round1-keyboard-navigation-fixture-qa.sh
+++ b/C3.tools/round1-keyboard-navigation-fixture-qa.sh
@@ -1,7 +1,8 @@
 #!/bin/bash
 # Round01 keyboard navigation fixture QA. It drives deterministic key commands
-# through the Quick Switch debug hook and verifies stable selection, no wrapping,
-# visible scrolling, Tab ignore, Enter activation reporting, and clean close.
+# through the Quick Switch debug hook and verifies vertical App-index focus,
+# column-local movement, visible scrolling, Tab ignore, Enter activation reporting,
+# and clean close.
 
 set -euo pipefail
 
@@ -11,9 +12,11 @@
 source "$SCRIPT_DIR/build-output-paths.sh"
 APP="$BUILD_CURRENT_APP"
 REPORT="$BUILD_REPORT_ROOT/round01-keyboard-navigation-fixture-report.json"
-FIXTURE_APP_COUNT="${ALIGNER_ROUND1_KEYBOARD_FIXTURE_APP_COUNT:-8}"
+FIXTURE_APP_COUNT="${ALIGNER_ROUND1_KEYBOARD_FIXTURE_APP_COUNT:-12}"
 FIXTURE_WINDOWS_PER_APP="${ALIGNER_ROUND1_KEYBOARD_FIXTURE_WINDOWS_PER_APP:-8}"
-KEY_SEQUENCE="${ALIGNER_ROUND1_KEYBOARD_SEQUENCE:-right,down,down,down,down,down,down,down,left,right,right,right,right,right,right,right,right,tab,enter}"
+KEY_SEQUENCE="${ALIGNER_ROUND1_KEYBOARD_SEQUENCE:-A,down,down,down,down,down,down,down,down,tab,enter}"
+EXPECTED_APP_INDEX="${ALIGNER_ROUND1_KEYBOARD_EXPECTED_APP_INDEX:-10}"
+EXPECTED_WINDOW_INDEX="${ALIGNER_ROUND1_KEYBOARD_EXPECTED_WINDOW_INDEX:-$((FIXTURE_WINDOWS_PER_APP - 1))}"
 REPORT_WAIT="${ALIGNER_ROUND1_KEYBOARD_REPORT_WAIT:-6.0}"
 
 fail() {
@@ -80,7 +83,7 @@
 }
 
 assert_report() {
-  /usr/bin/python3 - "$REPORT" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" "$KEY_SEQUENCE" <<'PY'
+  /usr/bin/python3 - "$REPORT" "$FIXTURE_APP_COUNT" "$FIXTURE_WINDOWS_PER_APP" "$KEY_SEQUENCE" "$EXPECTED_APP_INDEX" "$EXPECTED_WINDOW_INDEX" <<'PY'
 import json
 import sys
 
@@ -88,6 +91,8 @@
 fixture_app_count = int(sys.argv[2])
 fixture_windows_per_app = int(sys.argv[3])
 key_sequence = [part for part in sys.argv[4].split(",") if part]
+expected_selected_app_group_index = int(sys.argv[5])
+expected_selected_window_index = int(sys.argv[6])
 
 with open(path, "r", encoding="utf-8") as file:
     report = json.load(file)
@@ -101,6 +106,15 @@
 root = report.get("rootView", {})
 columns = root.get("waterfallColumns", [])
 items = root.get("appShelfItems", [])
+index_symbols = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+def normalized_key_command(command):
+    upper = command.upper()
+    if len(upper) == 1 and upper in index_symbols:
+        return f"app:{upper}"
+    if command == "tab":
+        return "tabIgnored"
+    return command
 
 require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true")
 require(report.get("quickSwitchVisible") is False, "Enter activation must close Quick Switch")
@@ -110,15 +124,19 @@
 require(len(columns) == fixture_app_count, "Waterfall column reports must match fixture count")
 require(len(items) == fixture_app_count, "App Shelf item reports must match fixture count")
 
-expected_selected_app_group_index = fixture_app_count - 1
-expected_selected_window_index = 0
 selected_column = columns[expected_selected_app_group_index]
 selected_card = selected_column.get("cards", [])[expected_selected_window_index]
 expected_selected_window_id = selected_card.get("windowID")
 
-require(root.get("selectedAppGroupIndex") == expected_selected_app_group_index, "right-arrow navigation must stop on the final App column")
-require(root.get("selectedWindowIndex") == expected_selected_window_index, "right-arrow into new columns must fall back to window index zero")
+require(root.get("selectedAppGroupIndex") == expected_selected_app_group_index, "index-key App focus plus down-arrow navigation must select the expected App column")
+require(root.get("selectedWindowIndex") == expected_selected_window_index, "down-arrow navigation must move within the focused App column")
 require(root.get("selectedWindowID") == expected_selected_window_id, "selectedWindowID must match selected card")
+require(root.get("keyboardFocusedAppGroupIndex") == expected_selected_app_group_index, "keyboard App focus must stay on the target App")
+require(root.get("hoveredAppGroupIndex") == expected_selected_app_group_index, "keyboard App focus must expose hoveredAppGroupIndex")
+require(root.get("hoverTargetKind") == "app", "keyboard App focus must use an App hover target")
+require(root.get("hoverTargetSource") == "keyboard", "keyboard App focus must report keyboard source")
+expected_app_index_command = next((normalized_key_command(command) for command in key_sequence if normalized_key_command(command).startswith("app:")), None)
+require(root.get("lastKeyboardAppIndexCommand") == expected_app_index_command, "App index command must be reported")
 require(report.get("lastCommittedWindowID") == expected_selected_window_id, "Enter commit must be surfaced at session level")
 require(report.get("lastCommitSource") == "keyboard", "Enter commit source must be keyboard")
 require(report.get("lastActivationWindowID") == expected_selected_window_id, "Enter activation window must match selected card")
@@ -128,9 +146,10 @@
 require(root.get("lastCommittedWindowIndex") == expected_selected_window_index, "Enter commit window index must match selected card")
 
 commands = root.get("keyboardCommandsApplied", [])
-require(commands == ["tabIgnored" if command == "tab" else command for command in key_sequence], "debug keyboard sequence must be applied in order with Tab ignored")
+require(commands == [normalized_key_command(command) for command in key_sequence], "debug keyboard sequence must be applied in order with App index and Tab normalized")
 require(root.get("tabIgnoredCount") == 1, "Tab must be ignored exactly once")
 require(root.get("lastKeyboardCommand") == "enter", "Enter must be the final keyboard command")
+require(root.get("boundaryBounceCount") == 0, "in-range down-arrow movement must not report a boundary bounce")
 
 selected_cards = [card for column in columns for card in column.get("cards", []) if card.get("isSelected") is True]
 require(len(selected_cards) == 1, "Waterfall must expose exactly one selected card after keyboard navigation")
@@ -138,23 +157,26 @@
 require("selectedVisualSuppressed" in selected_cards[0].get("visualStates", []), "non-hovered keyboard-selected Waterfall card must suppress selected visual state")
 require(selected_cards[0].get("shineVisible") is False, "non-hovered keyboard-selected Waterfall card must not expose shine")
 require(selected_cards[0].get("zPosition", 0) == 0, "non-hovered keyboard-selected Waterfall card must not float above normal cards")
+require("appLinked" in selected_column.get("cards", [])[0].get("visualStates", []), "App-focused vertical column must keep its first card linked to App hover")
 
 selected_items = [item for item in items if item.get("isSelected") is True]
 require(len(selected_items) == 1, "App Shelf must expose exactly one selected App")
 require(selected_items[0].get("index") == expected_selected_app_group_index, "App Shelf selection must follow keyboard-selected Waterfall column")
+require(selected_items[0].get("isHovered") is True, "App Shelf selected App must also expose keyboard hover")
+require("selected" in selected_items[0].get("visualStates", []), "keyboard-focused selected App Shelf item must expose selected visual")
+require("hover" in selected_items[0].get("visualStates", []), "keyboard-focused selected App Shelf item must expose hover visual")
 
 history = {
     entry.get("appGroupIndex"): entry.get("windowIndex")
     for entry in root.get("columnSelectionHistory", [])
 }
-require(history.get(1) == fixture_windows_per_app - 1, "left/right navigation must restore the remembered window index for column 1")
-require(history.get(expected_selected_app_group_index) == 0, "final column history must remember selected window zero")
+require(history.get(expected_selected_app_group_index) == expected_selected_window_index, "final column history must remember selected window")
 
 require(root.get("waterfallScrollable") is True, "fixture must make Waterfall horizontally scrollable")
 require(root.get("waterfallMaxScrollOffset", 0) > 0, "Waterfall must expose horizontal overflow")
 require(root.get("waterfallScrollOffset", 0) > 0, "selected far-right App column must auto-scroll into view")
-require(columns[1].get("maxVerticalScrollOffset", 0) > 0, "fixture column 1 must expose vertical overflow")
-require(columns[1].get("verticalScrollOffset", 0) > 0, "down-arrow navigation must auto-scroll the selected window into view")
+require(selected_column.get("maxVerticalScrollOffset", 0) > 0, "selected fixture column must expose vertical overflow")
+require(selected_column.get("verticalScrollOffset", 0) > 0, "down-arrow navigation must auto-scroll the selected window into view")
 
 frame = selected_cards[0].get("visibleFrame", {})
 require(frame.get("x", -1) >= 0, "selected Waterfall card must be horizontally visible")
@@ -167,12 +189,13 @@
     "selectedWindowIndex": root.get("selectedWindowIndex"),
     "selectedWindowID": root.get("selectedWindowID"),
     "lastCommittedWindowID": root.get("lastCommittedWindowID"),
-    "lastActivationResult": report.get("lastActivationResult"),
-    "quickSwitchVisible": report.get("quickSwitchVisible"),
-    "waterfallScrollOffset": root.get("waterfallScrollOffset"),
-    "column1VerticalScrollOffset": columns[1].get("verticalScrollOffset"),
-    "tabIgnoredCount": root.get("tabIgnoredCount")
-}, indent=2, ensure_ascii=False))
+	    "lastActivationResult": report.get("lastActivationResult"),
+	    "quickSwitchVisible": report.get("quickSwitchVisible"),
+	    "waterfallScrollOffset": root.get("waterfallScrollOffset"),
+	    "selectedColumnVerticalScrollOffset": selected_column.get("verticalScrollOffset"),
+	    "keyboardFocusedAppGroupIndex": root.get("keyboardFocusedAppGroupIndex"),
+	    "tabIgnoredCount": root.get("tabIgnoredCount")
+	}, indent=2, ensure_ascii=False))
 PY
 }
 

--
Gitblit v1.9.3