Files
openclaw/apps/ios/Tests/RuntimeLocalizationSourceGuardTests.swift
Peter Steinberger 0fff74c800 feat(ios): replace the tab bar with a black overlay sidebar (web parity + dashboard) (#111339)
* feat(ios): unify navigation on a black overlay sidebar and drop the phone tab bar

* test(ios): rework navigation tests and snapshot UITests for the sidebar shell

* chore(i18n): sync derived native catalogs for sidebar strings

* feat(ios): reveal the sidebar beneath a push-away rounded content card

* feat(ios): web-parity sidebar — agent switcher, pinned pages, sessions vocabulary, uniform content card

* chore(i18n): translate sidebar and session strings across native catalogs

* fix(ios): sidebar footer shows connection dot only when degraded, like the web

* feat(ios): sidebar toggle top-left with edge-swipe open; card surface owns the drawer chrome

* fix(ios): single sidebar refresh owner, alias-aware Home badges, RM tap-close, user pin order, drop vestigial idiom shims

* feat(ios): align sidebar with the mobile prototype — agent roster, theme-following palette, web session subtitles, row badges

* chore(i18n): translate agent roster and Recent strings across native catalogs

* feat(ios): frosted-glass agent picker with uniform drawer insets

* fix(ios): address sidebar review findings — settings toggle edge, per-agent scoping, alias-aware selection, cron paging, refresh cadence

* fix(ios): clear landing CI blockers
2026-07-19 14:50:39 -07:00

173 lines
8.5 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
struct RuntimeLocalizationSourceGuardTests {
@Test func `live activity state persists semantics and external detail`() throws {
for status in OpenClawActivityAttributes.ContentState.Status.allCases {
let state = OpenClawActivityAttributes.ContentState(
status: status,
verbatimDetail: status == .attention ? "Backend supplied detail" : nil,
startedAt: Date(timeIntervalSince1970: 1234),
voiceSamples: status == .voiceSpeaking ? [12, 96, 240] : nil)
let data = try JSONEncoder().encode(state)
let decoded = try JSONDecoder().decode(OpenClawActivityAttributes.ContentState.self, from: data)
#expect(decoded == state)
}
}
@Test func `live activity state decodes shipped legacy payloads`() throws {
let cases: [(LegacyContentState, OpenClawActivityAttributes.ContentState.Status, String?)] = [
(LegacyContentState(statusText: "Disconnected", isDisconnected: true), .disconnected, nil),
(LegacyContentState(statusText: "Idle", isIdle: true), .idle, nil),
(LegacyContentState(statusText: "Reconnecting...", isConnecting: true), .reconnecting, nil),
(LegacyContentState(statusText: "Ansluter igen...", isConnecting: true), .reconnecting, nil),
(LegacyContentState(statusText: "Approval needed"), .approvalNeeded, nil),
(LegacyContentState(statusText: "Backend supplied attention"), .attention, "Backend supplied attention"),
(
LegacyContentState(statusText: "Backend supplied connection detail", isConnecting: true),
.connecting,
"Backend supplied connection detail"),
]
for (legacy, expectedStatus, expectedDetail) in cases {
let data = try JSONEncoder().encode(legacy)
let decoded = try JSONDecoder().decode(OpenClawActivityAttributes.ContentState.self, from: data)
#expect(decoded.status == expectedStatus)
#expect(decoded.verbatimDetail == expectedDetail)
#expect(decoded.startedAt == legacy.startedAt)
}
}
@Test func `runtime owned copy remains localizable at render time`() throws {
let attributes = try Self.source("Sources/LiveActivity/OpenClawActivityAttributes.swift")
let manager = try Self.source("Sources/LiveActivity/LiveActivityManager.swift")
let widget = try Self.source("ActivityWidget/OpenClawLiveActivity.swift")
let project = try Self.source("project.yml")
let dreaming = try Self.source("Sources/Design/AgentProDreamingDestination.swift")
let rootSidebar = try Self.source("Sources/RootSidebar.swift")
let proComponents = try Self.source("Sources/Design/OpenClawProComponents.swift")
let skillWorkshop = try Self.source("Sources/Design/IPadSkillWorkshopScreen.swift")
let workboard = try Self.source("Sources/Design/IPadWorkboardScreen.swift")
let talkManager = try Self.source("Sources/Voice/TalkModeManager.swift")
let rootTabsNavigation = try Self.source("Sources/RootTabsNavigation.swift")
let watchInbox = try Self.source("WatchApp/Sources/WatchInboxView.swift")
let chat = try Self.sharedSource("OpenClawChatUI/ChatMessageViews.swift")
#expect(!attributes.contains("var statusText"))
#expect(attributes.contains("var status: Status"))
#expect(attributes.contains("var verbatimDetail: String?"))
#expect(attributes.contains("private enum LegacyCodingKeys"))
#expect(manager.contains("status: .disconnected"))
#expect(!manager.contains("statusText: String(localized: \"Disconnected\")"))
#expect(widget.contains("Text(verbatim: detail)"))
#expect(widget.contains("case .reconnecting: Text(\"Reconnecting...\")"))
#expect(project.contains("""
OpenClawActivityWidget:
"""))
#expect(project.contains("""
- path: Resources/Localizable.xcstrings
buildPhase: resources
"""))
#expect(dreaming.contains("AttributedString(localized: \"^[\\(recallCount) recall](inflect: true)\""))
#expect(dreaming.contains("format: String(localized: \"%@ grounded\")"))
#expect(dreaming.contains("parts.formatted(.list(type: .and, width: .short))"))
#expect(chat.contains("private var title: LocalizedStringResource"))
#expect(chat.contains("private var accessibilityText: LocalizedStringResource"))
#expect(chat.contains("Text(self.accessibilityText)"))
#expect(watchInbox.contains("case localized(LocalizedStringResource)"))
#expect(!watchInbox.contains("WatchTextValue: ExpressibleByStringLiteral"))
#expect(watchInbox.contains("accessory: .verbatim(self.store.talkSummaryText)"))
for status in ["Online", "Connecting", "Needs attention", "Offline"] {
#expect(rootSidebar.contains("String(localized: \"\(status)\")"))
}
let destinationTitles = [
"Chat",
"Overview",
"Activity",
"Agents",
"Workboard",
"Skill Workshop",
"Instances",
"Sessions",
"Files",
"Dreaming",
"Usage",
"Automations",
"Terminal",
"Docs",
"Settings",
"Settings / Gateway",
]
for title in destinationTitles {
#expect(rootTabsNavigation.contains("String(localized: \"\(title)\")"))
}
#expect(rootTabsNavigation.contains("case .gateway: String(localized: \"Connection\")"))
for status in ["Online", "Connecting", "Attention", "Offline"] {
#expect(proComponents.contains("String(localized: \"\(status)\")"))
}
#expect(rootSidebar.contains("String(localized: \"New Chat\")"))
#expect(proComponents.contains("OpenClawStatusBadge(label: .verbatim(self.title)"))
#expect(
skillWorkshop.components(separatedBy: "String(localized: \"Default agent\")").count - 1 == 2)
#expect(workboard.components(separatedBy: "String(localized: \"Default agent\")").count - 1 == 4)
#expect(!workboard.contains("?? \"Default agent\""))
#expect(talkManager.contains(
"var gatewayTalkActiveModeTitle: String = .init(localized: \"Not active\")"))
for title in [
"Not active",
"Paused",
"Realtime unavailable",
"iOS Speech + TTS",
"iOS Speech fallback",
] {
#expect(talkManager.contains("localized: \"\(title)\""))
}
#expect(!talkManager.contains("gatewayTalkActiveModeTitle: String = \""))
#expect(!talkManager.contains("gatewayTalkActiveModeTitle = \""))
}
@Test func `voice waveform stays on avatar without expanded contour`() throws {
let widget = try Self.source("ActivityWidget/OpenClawLiveActivity.swift")
#expect(!widget.contains("DynamicIslandExpandedRegion(.bottom)"))
#expect(!widget.contains("expandedVoiceContour"))
#expect(widget.contains(".keylineTint(self.islandKeylineTint(state: state))"))
#expect(widget.contains("case .voiceListening, .voiceActive:\n OpenClawActivityStyle.sea"))
#expect(widget.contains("TalkAvatarWaveformView("))
#expect(widget.contains("Text(\"LIVE\")"))
#expect(!widget.contains("compactVoiceLeading"))
#expect(!widget.contains("compactVoiceTrailing"))
#expect(!widget.contains("Color.clear\n .frame(width: 1, height: 1)"))
}
private static func source(_ path: String) throws -> String {
try String(
contentsOf: self.iosRoot.appendingPathComponent(path),
encoding: .utf8)
}
private static func sharedSource(_ path: String) throws -> String {
try String(
contentsOf: self.iosRoot
.deletingLastPathComponent()
.appendingPathComponent("shared/OpenClawKit/Sources")
.appendingPathComponent(path),
encoding: .utf8)
}
private static let iosRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
private struct LegacyContentState: Encodable {
let statusText: String
var isIdle = false
var isDisconnected = false
var isConnecting = false
var startedAt = Date(timeIntervalSince1970: 1234)
}
}