| C1.source/Resources/Aligner-Info.plist | ●●●●● patch | view | raw | blame | history | |
| C1.source/Sources/Aligner/QuickSwitchRootView.swift | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-keyboard-navigation-fixture-qa.sh | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-main-ui-qa.sh | ●●●●● patch | view | raw | blame | history | |
| C3.tools/round1-vertical-keyboard-app-focus-fixture-qa.sh | ●●●●● patch | view | raw | blame | history |
C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@ <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>0.0.72</string> <string>0.0.73</string> <key>CFBundleVersion</key> <string>20260618.2320</string> <string>20260619.1408</string> <key>LSMinimumSystemVersion</key> <string>26.0</string> <key>NSHighResolutionCapable</key> C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -70,6 +70,7 @@ private var currentSelection: QuickSwitchSelection? private var columnSelectionHistory: [Int: Int] = [:] private var keyboardFocusedAppGroupIndex: Int? private var keyboardFocusedWindowID: UInt32? private var keyboardCommandsApplied: [String] = [] private var lastKeyboardCommand: String? private var lastKeyboardAppIndexCommand: String? @@ -266,6 +267,7 @@ private struct WaterfallColumnLayers { let column: QuickSwitchWaterfallColumnViewModel let containerLayer: CALayer let cardsClipLayer: CALayer let headerLayer: CALayer let appNameLayer: CATextLayer let countLayer: CATextLayer @@ -355,6 +357,8 @@ static let cardGap: CGFloat = 12 static let revealPadding: CGFloat = 24 static let horizontalRevealPadding: CGFloat = 0 static let selectedCardScale: CGFloat = 1.015 static let selectedCardScaleOverflow: CGFloat = cardHeight * (selectedCardScale - 1) / 2 } private enum HorizontalMasonryMetrics { @@ -739,7 +743,8 @@ if case .realScreenshot = resolution.source { let isSelected = card.item.window.id == effectiveSelection?.windowID let isHovered = card.item.window.id == hoveredWindowID card.shineLayer.opacity = shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) ? 1 : 0 let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID card.shineLayer.opacity = (shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused) ? 1 : 0 } needsLayout = true } @@ -815,6 +820,12 @@ viewModel?.waterfallColumns.contains(where: { $0.appGroupIndex == keyboardFocusedAppGroupIndex }) != true { self.keyboardFocusedAppGroupIndex = nil } if let keyboardFocusedWindowID, viewModel?.waterfallColumns.contains(where: { column in column.windows.contains { $0.window.id == keyboardFocusedWindowID } }) != true { self.keyboardFocusedWindowID = nil } if let currentSelection { columnSelectionHistory[currentSelection.appGroupIndex] = currentSelection.windowIndex } @@ -830,6 +841,7 @@ currentSelection = viewModel?.initialSelection columnSelectionHistory = [:] keyboardFocusedAppGroupIndex = nil keyboardFocusedWindowID = nil keyboardCommandsApplied = [] lastKeyboardCommand = nil lastKeyboardAppIndexCommand = nil @@ -1256,6 +1268,7 @@ "selectedWindowIndex": effectiveSelection?.windowIndex ?? NSNull(), "selectedWindowID": effectiveSelection?.windowID ?? NSNull(), "keyboardFocusedAppGroupIndex": keyboardFocusedAppGroupIndex ?? NSNull(), "keyboardFocusedWindowID": keyboardFocusedWindowID ?? NSNull(), "keyboardCommandsApplied": keyboardCommandsApplied, "lastKeyboardCommand": lastKeyboardCommand ?? NSNull(), "lastKeyboardAppIndexCommand": lastKeyboardAppIndexCommand ?? NSNull(), @@ -1450,6 +1463,7 @@ } keyboardFocusedAppGroupIndex = appGroupIndex keyboardFocusedWindowID = nil lastKeyboardAppIndexCommand = appIndexCommand selectionChangedByLastCommand = false clearKeyboardBoundaryFeedback() @@ -1533,6 +1547,7 @@ keyboardFocusedAppGroupIndex = targetAppGroupIndex currentSelection = nextSelection keyboardFocusedWindowID = nextSelection.windowID columnSelectionHistory[nextSelection.appGroupIndex] = nextSelection.windowIndex selectionChangedByLastCommand = previousSelection != nextSelection clearKeyboardBoundaryFeedback() @@ -2767,8 +2782,13 @@ container.borderColor = NSColor.separatorColor.withAlphaComponent(0.34).cgColor container.borderWidth = 1 let cardsClip = CALayer() cardsClip.masksToBounds = true cardsClip.backgroundColor = NSColor.clear.cgColor let header = CALayer() header.backgroundColor = NSColor.controlBackgroundColor.withAlphaComponent(0.68).cgColor header.zPosition = 40 let appName = makeTextLayer( string: column.app.name, @@ -2785,6 +2805,10 @@ alignment: .right ) appName.zPosition = 42 count.zPosition = 42 container.addSublayer(cardsClip) container.addSublayer(header) container.addSublayer(appName) container.addSublayer(count) @@ -2851,7 +2875,7 @@ card.addSublayer(state) card.addSublayer(shine) card.addSublayer(closeButton.containerLayer) container.addSublayer(card) cardsClip.addSublayer(card) return WaterfallCardLayers( item: window, @@ -2872,6 +2896,7 @@ return WaterfallColumnLayers( column: column, containerLayer: container, cardsClipLayer: cardsClip, headerLayer: header, appNameLayer: appName, countLayer: count, @@ -3591,7 +3616,14 @@ x: locationInWaterfallContent.x - column.containerLayer.frame.minX, y: locationInWaterfallContent.y - column.containerLayer.frame.minY ) if let card = column.cards.first(where: { $0.containerLayer.frame.contains(locationInColumn) }) { guard column.cardsClipLayer.frame.contains(locationInColumn) else { continue } let locationInCardsClip = CGPoint( x: locationInColumn.x - column.cardsClipLayer.frame.minX, y: locationInColumn.y - column.cardsClipLayer.frame.minY ) if let card = column.cards.first(where: { $0.containerLayer.frame.contains(locationInCardsClip) }) { return card } } @@ -4154,6 +4186,8 @@ column.containerLayer.shadowOpacity = 0 column.containerLayer.shadowRadius = 0 column.containerLayer.shadowOffset = .zero column.cardsClipLayer.frame = column.containerLayer.bounds column.cardsClipLayer.masksToBounds = false column.headerLayer.isHidden = true column.appNameLayer.isHidden = true column.countLayer.isHidden = true @@ -4293,6 +4327,14 @@ column.countLayer.isHidden = false let bounds = column.containerLayer.bounds let cardsClipFrame = CGRect( x: 0, y: 0, width: bounds.width, height: max(1, bounds.height - WaterfallMetrics.headerHeight) ) column.cardsClipLayer.frame = cardsClipFrame column.cardsClipLayer.masksToBounds = true let isSpaceFocused = spaceFocusWindowCount(for: column.column.appGroupIndex) > 0 let isHovered = column.column.appGroupIndex == effectiveHoveredAppGroupIndex column.containerLayer.borderColor = (isSpaceFocused @@ -4335,8 +4377,8 @@ waterfallColumnScrollOffsets[appGroupIndex] = verticalOffset let cardWidth = max(1, bounds.width - WaterfallMetrics.columnHorizontalPadding * 2) let firstCardTop = bounds.height - WaterfallMetrics.headerHeight let firstCardTop = cardsClipFrame.height - WaterfallMetrics.selectedCardScaleOverflow - WaterfallMetrics.columnVerticalPadding + verticalOffset @@ -4360,10 +4402,11 @@ let bounds = card.containerLayer.bounds let isSelected = card.item.window.id == effectiveSelection?.windowID let isHovered = card.item.window.id == hoveredWindowID let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID let isAppLinked = isWaterfallCardAppLinked(card) applyWaterfallCardVisualState( card, selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered), selected: shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused, hovered: isHovered, appLinked: isAppLinked ) @@ -4469,7 +4512,7 @@ ? CGSize(width: 0, height: -5) : .zero card.containerLayer.zPosition = selected ? 20 : hovered ? 12 : appLinked ? 10 : isSpaceFocused ? 8 : 0 let focusedScale: CGFloat = selected ? 1.015 : isSpaceFocused ? 1.012 : 1.008 let focusedScale: CGFloat = selected ? WaterfallMetrics.selectedCardScale : isSpaceFocused ? 1.012 : 1.008 card.containerLayer.transform = (selected || hovered || isSpaceFocused) ? CATransform3DMakeScale(focusedScale, focusedScale, 1) : CATransform3DIdentity @@ -4945,11 +4988,18 @@ x: locationInWaterfallContent.x - column.containerLayer.frame.minX, y: locationInWaterfallContent.y - column.containerLayer.frame.minY ) for card in column.cards where card.containerLayer.frame.contains(locationInColumn) { guard column.cardsClipLayer.frame.contains(locationInColumn) else { continue } let locationInCardsClip = CGPoint( x: locationInColumn.x - column.cardsClipLayer.frame.minX, y: locationInColumn.y - column.cardsClipLayer.frame.minY ) for card in column.cards where card.containerLayer.frame.contains(locationInCardsClip) { guard card.closeButton.containerLayer.opacity > 0 else { continue } let locationInCard = CGPoint( x: locationInColumn.x - card.containerLayer.frame.minX, y: locationInColumn.y - card.containerLayer.frame.minY x: locationInCardsClip.x - card.containerLayer.frame.minX, y: locationInCardsClip.y - card.containerLayer.frame.minY ) if card.closeButton.containerLayer.frame.insetBy(dx: -8, dy: -8).contains(locationInCard) { return .window( @@ -4991,11 +5041,13 @@ x: waterfallLayer.frame.minX + waterfallContentLayer.frame.minX + column.containerLayer.frame.minX + column.cardsClipLayer.frame.minX + card.containerLayer.frame.minX + card.closeButton.containerLayer.frame.midX, y: waterfallLayer.frame.minY + waterfallContentLayer.frame.minY + column.containerLayer.frame.minY + column.cardsClipLayer.frame.minY + card.containerLayer.frame.minY + card.closeButton.containerLayer.frame.midY ) @@ -5123,7 +5175,7 @@ ) let contentHeight = CGFloat(column.cards.count) * WaterfallMetrics.cardHeight + CGFloat(max(0, column.cards.count - 1)) * WaterfallMetrics.cardGap return max(0, contentHeight - visibleHeight) return max(0, contentHeight - visibleHeight + WaterfallMetrics.selectedCardScaleOverflow * 2) } private func clampedWaterfallColumnOffset(_ offset: CGFloat, appGroupIndex: Int) -> CGFloat { @@ -5546,10 +5598,13 @@ let appGroupIndex = column.column.appGroupIndex let isHovered = appGroupIndex == effectiveHoveredAppGroupIndex let firstCardFrame = column.cards.first?.containerLayer.frame let headerToFirstCardGap = firstCardFrame.map { let visibleFirstCardFrame = firstCardFrame.map { $0.offsetBy(dx: column.cardsClipLayer.frame.minX, dy: column.cardsClipLayer.frame.minY) } let headerToFirstCardGap = visibleFirstCardFrame.map { column.headerLayer.frame.minY - $0.maxY } let topToFirstCardGap = firstCardFrame.map { let topToFirstCardGap = visibleFirstCardFrame.map { column.containerLayer.bounds.height - $0.maxY } @@ -5567,6 +5622,8 @@ "visibleFrame": dictionary(from: visibleFrame), "headerFrame": dictionary(from: column.headerLayer.frame), "headerBackgroundColor": colorDictionary(from: column.headerLayer.backgroundColor), "cardsClipFrame": dictionary(from: column.cardsClipLayer.frame), "cardsClipMasksToBounds": column.cardsClipLayer.masksToBounds, "headerToFirstCardGap": headerToFirstCardGap.map(Double.init) ?? NSNull(), "topToFirstCardGap": topToFirstCardGap.map(Double.init) ?? NSNull(), "appNameFrame": dictionary(from: column.appNameLayer.frame), @@ -5631,8 +5688,8 @@ private func waterfallCardReports(for column: WaterfallColumnLayers) -> [[String: Any]] { column.cards.map { card in let visibleFrame = card.containerLayer.frame.offsetBy( dx: column.containerLayer.frame.minX + waterfallContentLayer.frame.minX, dy: column.containerLayer.frame.minY dx: column.containerLayer.frame.minX + column.cardsClipLayer.frame.minX + waterfallContentLayer.frame.minX, dy: column.containerLayer.frame.minY + column.cardsClipLayer.frame.minY ) let screenshotSource = screenshotSourcesByWindowID[card.item.window.id] @@ -5650,6 +5707,7 @@ "isFullscreen": card.item.window.isFullscreen, "identifierSource": "\(card.item.window.identifierSource)", "isSelected": card.item.window.id == effectiveSelection?.windowID, "isKeyboardFocused": card.item.window.id == keyboardFocusedWindowID, "visualStates": waterfallCardVisualStates(for: card), "thumbnailStrategy": thumbnailStrategyString(for: card.item.window), "screenshotSource": screenshotSourceString(screenshotSource, for: card.item.window), @@ -5768,12 +5826,16 @@ var states: [String] = [] let isSelected = card.item.window.id == effectiveSelection?.windowID let isHovered = card.item.window.id == hoveredWindowID let isKeyboardFocused = card.item.window.id == keyboardFocusedWindowID let isAppLinked = isWaterfallCardAppLinked(card) if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) { if shouldShowSelectedVisual(isSelected: isSelected, isHovered: isHovered) || isKeyboardFocused { states.append("selected") } else if isSelected { states.append("selectedVisualSuppressed") } if isKeyboardFocused { states.append("keyboardFocused") } if isHovered { states.append("hover") } C3.tools/round1-keyboard-navigation-fixture-qa.sh
@@ -154,9 +154,12 @@ 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") require(selected_cards[0].get("windowID") == expected_selected_window_id, "selected Waterfall card must match selectedWindowID") 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(selected_cards[0].get("isKeyboardFocused") is True, "keyboard-selected Waterfall card must expose keyboard focus") require("selected" in selected_cards[0].get("visualStates", []), "keyboard-selected Waterfall card must expose selected visual state") require("keyboardFocused" in selected_cards[0].get("visualStates", []), "keyboard-selected Waterfall card must expose keyboardFocused visual state") require("selectedVisualSuppressed" not in selected_cards[0].get("visualStates", []), "keyboard-selected Waterfall card must not suppress selected visual state") require(selected_cards[0].get("shineVisible") is True, "keyboard-selected Waterfall card must expose shine") require(selected_cards[0].get("zPosition", 0) > 0, "keyboard-selected Waterfall card must 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] @@ -179,10 +182,19 @@ require(selected_column.get("verticalScrollOffset", 0) > 0, "down-arrow navigation must auto-scroll the selected window into view") frame = selected_cards[0].get("visibleFrame", {}) column_frame = selected_column.get("visibleFrame", {}) header_frame = selected_column.get("headerFrame", {}) cards_clip_frame = selected_column.get("cardsClipFrame", {}) visibility_tolerance = 1 require(frame.get("x", -1) >= 0, "selected Waterfall card must be horizontally visible") require(frame.get("x", 0) + frame.get("width", 0) <= root.get("waterfallVisibleWidth", 0) + 1, "selected Waterfall card must fit in visible Waterfall width") require(frame.get("y", -1) >= 0, "selected Waterfall card must be vertically visible") require(frame.get("y", -1) >= -visibility_tolerance, "selected Waterfall card must be vertically visible") require(frame.get("y", 0) + frame.get("height", 0) <= root.get("waterfallFrame", {}).get("height", 0) + 1, "selected Waterfall card must fit in visible Waterfall height") require(selected_column.get("cardsClipMasksToBounds") is True, "vertical Waterfall column must clip cards below the pinned header") require(cards_clip_frame.get("y", -1) == 0, "cards clip layer must start below the column bottom edge") require(cards_clip_frame.get("height", 0) <= header_frame.get("y", 0) + 1, "cards clip layer must stop before the pinned header") header_min_y = column_frame.get("y", 0) + header_frame.get("y", 0) require(frame.get("y", 0) + frame.get("height", 0) <= header_min_y + 1, "selected Waterfall card must not overlap the pinned column header") print(json.dumps({ "selectedAppGroupIndex": root.get("selectedAppGroupIndex"), C3.tools/round1-main-ui-qa.sh
@@ -109,6 +109,7 @@ run_step candidate-filter "$SCRIPT_DIR/round1-candidate-filter-fixture-qa.sh" run_step screenshot-session "$SCRIPT_DIR/round1-screenshot-session-qa.sh" run_step keyboard-navigation "$SCRIPT_DIR/round1-keyboard-navigation-fixture-qa.sh" run_step vertical-keyboard-app-focus "$SCRIPT_DIR/round1-vertical-keyboard-app-focus-fixture-qa.sh" run_step window-activation "$SCRIPT_DIR/round1-window-activation-fixture-qa.sh" run_step multi-page-identity "$SCRIPT_DIR/round1-multi-page-identity-fixture-qa.sh" run_step mouse-interaction "$SCRIPT_DIR/round1-mouse-interaction-fixture-qa.sh" C3.tools/round1-vertical-keyboard-app-focus-fixture-qa.sh
@@ -121,20 +121,24 @@ assert_report() { local report="$1" local expected_sequence="$2" local expected_app_index="$3" local expected_window_index="$4" local expected_boundary_count="$5" local expected_boundary_direction="$6" local expect_selection_changed="$7" local expected_focus_app_index="$3" local expected_selected_app_index="$4" local expected_window_index="$5" local expected_boundary_count="$6" local expected_boundary_direction="$7" local expect_selection_changed="$8" local expect_keyboard_window_focus="$9" /usr/bin/python3 - \ "$report" \ "$expected_sequence" \ "$expected_app_index" \ "$expected_focus_app_index" \ "$expected_selected_app_index" \ "$expected_window_index" \ "$expected_boundary_count" \ "$expected_boundary_direction" \ "$expect_selection_changed" \ "$expect_keyboard_window_focus" \ "$FIXTURE_APP_COUNT" \ "$FIXTURE_WINDOWS_PER_APP" <<'PY' import json @@ -142,13 +146,15 @@ path = sys.argv[1] expected_sequence = [part for part in sys.argv[2].split(",") if part] expected_app_index = int(sys.argv[3]) expected_window_index = int(sys.argv[4]) expected_boundary_count = int(sys.argv[5]) expected_boundary_direction = None if sys.argv[6] == "none" else sys.argv[6] expect_selection_changed = sys.argv[7] == "true" fixture_app_count = int(sys.argv[8]) fixture_windows_per_app = int(sys.argv[9]) expected_focus_app_index = int(sys.argv[3]) expected_selected_app_index = int(sys.argv[4]) expected_window_index = int(sys.argv[5]) expected_boundary_count = int(sys.argv[6]) expected_boundary_direction = None if sys.argv[7] == "none" else sys.argv[7] expect_selection_changed = sys.argv[8] == "true" expect_keyboard_window_focus = sys.argv[9] == "true" fixture_app_count = int(sys.argv[10]) fixture_windows_per_app = int(sys.argv[11]) with open(path, "r", encoding="utf-8") as file: report = json.load(file) @@ -162,8 +168,9 @@ root = report.get("rootView", {}) items = root.get("appShelfItems", []) columns = root.get("waterfallColumns", []) target_item = next((item for item in items if item.get("index") == expected_app_index), None) target_column = next((column for column in columns if column.get("appGroupIndex") == expected_app_index), None) focus_item = next((item for item in items if item.get("index") == expected_focus_app_index), None) focus_column = next((column for column in columns if column.get("appGroupIndex") == expected_focus_app_index), None) selected_column = next((column for column in columns if column.get("appGroupIndex") == expected_selected_app_index), None) require(report.get("snapshotLoaded") is True, "snapshotLoaded must be true") require(report.get("quickSwitchVisible") is True, "fixture must keep Quick Switch visible") @@ -171,22 +178,43 @@ require(report.get("windowCount") == fixture_app_count * fixture_windows_per_app, "fixture windowCount must match") require(root.get("waterfallViewMode") == "verticalColumns", "fixture must run in vertical Waterfall") require(root.get("keyboardCommandsApplied") == expected_sequence, "keyboard commands must match normalized sequence") require(root.get("keyboardFocusedAppGroupIndex") == expected_app_index, "keyboard focus must land on expected App") require(root.get("hoveredAppGroupIndex") == expected_app_index, "keyboard focus must expose hovered App") require(root.get("keyboardFocusedAppGroupIndex") == expected_focus_app_index, "keyboard focus must land on expected App") require(root.get("hoveredAppGroupIndex") == expected_focus_app_index, "keyboard focus must expose hovered App") require(root.get("hoverTargetKind") == "app", "keyboard focus must use App hover target") require(root.get("hoverTargetSource") == "keyboard", "keyboard focus must report keyboard source") require(root.get("lastCommittedWindowID") is None, "no-Enter keyboard focus runs must not commit") require(root.get("lastCommitSource") is None, "no-Enter keyboard focus runs must not report commit source") require(target_item is not None, "target App Shelf item must exist") require(target_column is not None, "target Waterfall column must exist") require(target_item.get("isHovered") is True, "target App Shelf item must expose hover") require("hover" in target_item.get("visualStates", []), "target App Shelf item must expose hover visual") require(target_column.get("isHovered") is True, "target Waterfall column must expose hover") require("hover" in target_column.get("visualStates", []), "target Waterfall column must expose hover visual") require(root.get("selectedAppGroupIndex") == expected_app_index, "selected App must match expected App after column entry") require(focus_item is not None, "focused App Shelf item must exist") require(focus_column is not None, "focused Waterfall column must exist") require(selected_column is not None, "selected Waterfall column must exist") selected_cards = [card for column in columns for card in column.get("cards", []) if card.get("isSelected") is True] selected_card = selected_column.get("cards", [])[expected_window_index] require(focus_item.get("isHovered") is True, "focused App Shelf item must expose hover") require("hover" in focus_item.get("visualStates", []), "focused App Shelf item must expose hover visual") require(focus_column.get("isHovered") is True, "focused Waterfall column must expose hover") require("hover" in focus_column.get("visualStates", []), "focused Waterfall column must expose hover visual") require(root.get("selectedAppGroupIndex") == expected_selected_app_index, "selected App must match expected selected App") require(root.get("selectedWindowIndex") == expected_window_index, "selected window index must match expected column movement") require(root.get("selectionChangedByLastCommand") is expect_selection_changed, "selectionChangedByLastCommand must match final command") require(root.get("boundaryBounceCount") == expected_boundary_count, "boundary bounce count must match") if expect_keyboard_window_focus: require(len(selected_cards) == 1, "keyboard window focus run must expose exactly one selected Waterfall card") require(selected_card.get("isSelected") is True, "expected selected card must expose selection") require(selected_card.get("isKeyboardFocused") is True, "expected selected card must expose keyboard focus") require(root.get("keyboardFocusedWindowID") == selected_card.get("windowID"), "keyboardFocusedWindowID must match selected card") require("selected" in selected_card.get("visualStates", []), "keyboard-focused selected card must expose selected visual") require("keyboardFocused" in selected_card.get("visualStates", []), "keyboard-focused selected card must expose keyboardFocused state") require(selected_card.get("shineVisible") is True, "keyboard-focused selected card must expose shine") header_frame = selected_column.get("headerFrame", {}) clip_frame = selected_column.get("cardsClipFrame", {}) column_frame = selected_column.get("visibleFrame", {}) card_frame = selected_card.get("visibleFrame", {}) require(selected_column.get("cardsClipMasksToBounds") is True, "vertical cards must be clipped below pinned header") require(clip_frame.get("height", 0) <= header_frame.get("y", 0) + 1, "cards clip must stop before pinned header") header_min_y = column_frame.get("y", 0) + header_frame.get("y", 0) require(card_frame.get("y", 0) + card_frame.get("height", 0) <= header_min_y + 1, "keyboard-focused card must not overlap pinned header") else: require(root.get("keyboardFocusedWindowID") is None, "App-only keyboard focus must not expose keyboardFocusedWindowID") if expected_boundary_direction is None: require(root.get("lastBoundaryBounceAxis") is None, "no-boundary run must not report boundary axis") require(root.get("lastBoundaryBounceDirection") is None, "no-boundary run must not report boundary direction") @@ -197,7 +225,9 @@ print(json.dumps({ "commands": root.get("keyboardCommandsApplied"), "keyboardFocusedAppGroupIndex": root.get("keyboardFocusedAppGroupIndex"), "selectedAppGroupIndex": root.get("selectedAppGroupIndex"), "selectedWindowIndex": root.get("selectedWindowIndex"), "keyboardFocusedWindowID": root.get("keyboardFocusedWindowID"), "boundaryBounceCount": root.get("boundaryBounceCount"), "lastBoundaryBounceDirection": root.get("lastBoundaryBounceDirection") }, indent=2, ensure_ascii=False)) @@ -207,11 +237,13 @@ run_case() { local name="$1" local sequence="$2" local expected_app_index="$3" local expected_window_index="$4" local expected_boundary_count="$5" local expected_boundary_direction="$6" local expect_selection_changed="$7" local expected_focus_app_index="$3" local expected_selected_app_index="$4" local expected_window_index="$5" local expected_boundary_count="$6" local expected_boundary_direction="$7" local expect_selection_changed="$8" local expect_keyboard_window_focus="$9" local report="$REPORT_DIR/round01-vertical-keyboard-$name-report.json" local expected_sequence expected_sequence="$(normalize_keyboard_sequence "$sequence")" @@ -235,11 +267,13 @@ assert_report \ "$report" \ "$expected_sequence" \ "$expected_app_index" \ "$expected_focus_app_index" \ "$expected_selected_app_index" \ "$expected_window_index" \ "$expected_boundary_count" \ "$expected_boundary_direction" \ "$expect_selection_changed" "$expect_selection_changed" \ "$expect_keyboard_window_focus" kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true @@ -254,14 +288,23 @@ if [ "$FIXTURE_WINDOWS_PER_APP" -lt 8 ]; then fail "fixture windows per app must be at least 8" fi symbols="1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" if [ "$FIXTURE_APP_COUNT" -gt "${#symbols}" ]; then fail "fixture app count cannot exceed supported App Shelf index symbols" fi LAST_APP_INDEX=$((FIXTURE_APP_COUNT - 1)) LAST_APP_SYMBOL="${symbols:LAST_APP_INDEX:1}" trap cleanup EXIT stop_current_aligner "$SCRIPT_DIR/package-app.sh" >&2 run_case "wrap" "1,left,right" 0 0 0 "none" "false" run_case "bottom-boundary" "3,down,down,down,down,down,down,down,down,down" 2 7 1 "down" "false" run_case "top-boundary" "4,down,up" 3 0 1 "up" "false" run_case "index-hover-only" "A" 10 0 0 0 "none" "false" "false" run_case "left-wrap-to-last" "1,left" "$LAST_APP_INDEX" 0 0 0 "none" "false" "false" run_case "right-wrap-to-first" "$LAST_APP_SYMBOL,right" 0 0 0 0 "none" "false" "false" run_case "bottom-boundary" "3,down,down,down,down,down,down,down,down,down" 2 2 7 1 "down" "false" "true" run_case "top-boundary" "4,down,up" 3 3 0 1 "up" "false" "true" trap - EXIT cleanup