#!/usr/bin/env bash
|
set -euo pipefail
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
QUICK_SEARCH="$ROOT_DIR/Apptag/QuickSearch.swift"
|
CONTENT_VIEW="$ROOT_DIR/Apptag/ContentView.swift"
|
TAG_NAV="$ROOT_DIR/Apptag/TagNavigationView.swift"
|
|
python3 - "$QUICK_SEARCH" "$CONTENT_VIEW" "$TAG_NAV" <<'PY'
|
import re
|
import sys
|
from pathlib import Path
|
|
quick_search = Path(sys.argv[1]).read_text(encoding="utf-8")
|
content_view = Path(sys.argv[2]).read_text(encoding="utf-8")
|
tag_nav = Path(sys.argv[3]).read_text(encoding="utf-8")
|
|
|
def fail(message: str) -> None:
|
print(f"FAIL: {message}", file=sys.stderr)
|
sys.exit(1)
|
|
|
def require(condition: bool, message: str) -> None:
|
if not condition:
|
fail(message)
|
|
|
def require_contains(source: str, needle: str, message: str) -> None:
|
require(needle in source, message)
|
|
|
require_contains(
|
quick_search,
|
"tagLauncherQuickSearchCompositionChanged",
|
"Quick Search must publish IME composition state",
|
)
|
require_contains(
|
quick_search,
|
"if !field.isComposingText, field.stringValue != text",
|
"Quick Search updateNSView must not overwrite marked text",
|
)
|
require_contains(
|
quick_search,
|
"if textView.hasMarkedText()",
|
"Quick Search doCommandBy must pass marked text commands through to IME",
|
)
|
require_contains(
|
quick_search,
|
"guard !isComposingText else",
|
"Quick Search keyDown must pass marked text key events through to IME",
|
)
|
require_contains(
|
quick_search,
|
"guard field?.isComposingText != true else { return }",
|
"Quick Search submit fallback must not launch while composing text",
|
)
|
|
apply_match = re.search(
|
r"func apply\(_ configuration: QuickSearchPanelConfiguration, anchorView: QuickSearchPanelAnchorView\).*?func closePanel\(\)",
|
quick_search,
|
re.S,
|
)
|
require(apply_match is not None, "Quick Search panel apply block is missing")
|
apply_body = apply_match.group(0)
|
require_contains(
|
apply_body,
|
"let shouldOrderForInput = shouldOrderPanelForInput(panel)",
|
"Quick Search panel must compute whether ordering is actually needed",
|
)
|
require_contains(
|
apply_body,
|
"if shouldOrderForInput",
|
"Quick Search panel activate/orderFront must be gated",
|
)
|
require_contains(
|
quick_search,
|
"field.currentEditor() == nil",
|
"Quick Search focus retry must stop once the field editor is active",
|
)
|
|
require_contains(
|
content_view,
|
"@State private var quickSearchCompositionActive = false",
|
"ContentView must track Quick Search composition state",
|
)
|
require_contains(
|
content_view,
|
"shouldIgnoreQuickSearchDismissDuringComposition",
|
"ContentView must ignore mouse/backdrop dismiss while composing text",
|
)
|
require_contains(
|
content_view,
|
"source == QuickSearchDismissSource.mouse || source == QuickSearchDismissSource.backdrop",
|
"composition dismiss guard must be scoped to mouse/backdrop dismissals",
|
)
|
|
mouse_match = re.search(
|
r"override func mouseDown\(with event: NSEvent\).*?@objc private func performActivation",
|
tag_nav,
|
re.S,
|
)
|
require(mouse_match is not None, "TagNavigation mouseDown block is missing")
|
mouse_body = mouse_match.group(0)
|
for needle, message in [
|
("let maxTrackingDuration: TimeInterval = 8", "TagNavigation tracking loop must have a hard time limit"),
|
("window.isVisible", "TagNavigation tracking loop must exit when the window is gone or hidden"),
|
("NSApp.isActive", "TagNavigation tracking loop must exit when the app loses active state"),
|
("NSEvent.pressedMouseButtons & (1 << 0) == 0", "TagNavigation tracking loop must recover if mouseUp is lost"),
|
("onReorderEnded()", "TagNavigation abnormal exits must clean up an active reorder"),
|
]:
|
require_contains(mouse_body, needle, message)
|
|
print("PASS windowserver event safety QA")
|
PY
|