mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 23:52:40 -06:00
8a12343efe
* test(ios): align source guards with current contracts * test(ios): stabilize cross-version UI automation * test(ios): harden typography boundary audit * test(ios): synchronize deterministic UI launches
857 lines
41 KiB
Swift
857 lines
41 KiB
Swift
import CoreText
|
|
import Foundation
|
|
import Testing
|
|
import UIKit
|
|
@testable import OpenClaw
|
|
|
|
struct RootSidebarTypographyTests {
|
|
@Test func `root sidebar uses branded typography`() throws {
|
|
let sidebar = try String(
|
|
contentsOf: URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("Sources/RootSidebar.swift"),
|
|
encoding: .utf8)
|
|
|
|
#expect(sidebar.contains("OpenClawType.headline"))
|
|
#expect(sidebar.contains("OpenClawType.subheadSemiBold"))
|
|
#expect(sidebar.contains("OpenClawType.captionMedium"))
|
|
#expect(sidebar.contains("OpenClawType.caption2Bold"))
|
|
#expect(!sidebar.contains(".font(."))
|
|
}
|
|
}
|
|
|
|
struct OpenClawTypographyTests {
|
|
@Test func `thread controls use branded typography`() throws {
|
|
let support = try String(
|
|
contentsOf: Self.sourceURL("Design/CommandCenterSupport.swift"),
|
|
encoding: .utf8)
|
|
let commandCenter = try String(
|
|
contentsOf: Self.sourceURL("Design/CommandCenterTab.swift"),
|
|
encoding: .utf8)
|
|
|
|
#expect(support.contains("TextField(self.editorPlaceholder"))
|
|
#expect(support.contains("Label(\"Move to Group\""))
|
|
#expect(support.contains("Label(\"Delete…\""))
|
|
#expect(support.contains(".font(OpenClawType.subhead)"))
|
|
#expect(support.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(commandCenter.contains("Toggle(isOn: self.$showArchived)"))
|
|
#expect(commandCenter.contains("Text(\"Show Archived\")"))
|
|
#expect(commandCenter.contains(".font(OpenClawType.captionMedium)"))
|
|
}
|
|
|
|
@Test func `bundled fonts load from app bundle`() {
|
|
for name in OpenClawType.registeredPostScriptNames {
|
|
#expect(UIFont(name: name, size: 12) != nil, "Missing bundled font: \(name)")
|
|
}
|
|
}
|
|
|
|
@Test func `dynamic type scales display fonts`() {
|
|
guard let base = UIFont(name: "RedHatDisplay-Regular", size: 34) else {
|
|
Issue.record("RedHatDisplay-Regular should be bundled")
|
|
return
|
|
}
|
|
|
|
let defaultTraits = UITraitCollection(preferredContentSizeCategory: .large)
|
|
let largeTraits = UITraitCollection(preferredContentSizeCategory: .accessibilityExtraExtraExtraLarge)
|
|
let metrics = UIFontMetrics(forTextStyle: .largeTitle)
|
|
|
|
let defaultSize = metrics.scaledFont(for: base, compatibleWith: defaultTraits).pointSize
|
|
let largeSize = metrics.scaledFont(for: base, compatibleWith: largeTraits).pointSize
|
|
|
|
#expect(largeSize > defaultSize)
|
|
}
|
|
|
|
@Test func `display variable weight axis instantiates heavy weights`() {
|
|
guard UIFont(name: "RedHatDisplay-Regular", size: 15) != nil else {
|
|
Issue.record("RedHatDisplay-Regular should be bundled")
|
|
return
|
|
}
|
|
|
|
let weightAxis = NSNumber(value: 2_003_265_652) // "wght"
|
|
let descriptor = UIFontDescriptor(fontAttributes: [
|
|
.name: "RedHatDisplay-Regular",
|
|
kCTFontVariationAttribute as UIFontDescriptor.AttributeName: [weightAxis: 900],
|
|
])
|
|
let font = UIFont(descriptor: descriptor, size: 15)
|
|
let variations = font.fontDescriptor.object(
|
|
forKey: kCTFontVariationAttribute as UIFontDescriptor.AttributeName) as? [NSNumber: Any]
|
|
let weightValue = variations?[weightAxis] as? NSNumber
|
|
|
|
#expect(weightValue?.doubleValue == 900)
|
|
}
|
|
|
|
@Test func `app extensions register bundled branded fonts`() throws {
|
|
let project = try String(contentsOf: Self.projectYmlURL(), encoding: .utf8)
|
|
let activityPlist = try String(contentsOf: Self.activityWidgetInfoPlistURL(), encoding: .utf8)
|
|
let watchPlist = try String(contentsOf: Self.watchInfoPlistURL(), encoding: .utf8)
|
|
|
|
for targetName in ["OpenClawActivityWidget", "OpenClawWatchApp"] {
|
|
let target = try Self.extract(
|
|
project,
|
|
from: " \(targetName):",
|
|
to: targetName == "OpenClawActivityWidget" ? " OpenClawWatchApp:" : " OpenClawTests:")
|
|
#expect(target.contains("- path: Sources/Fonts"))
|
|
#expect(target.contains("UIAppFonts:"))
|
|
for font in Self.bundledFontFiles {
|
|
#expect(target.contains("- \(font)"))
|
|
}
|
|
}
|
|
|
|
for plist in [activityPlist, watchPlist] {
|
|
#expect(plist.contains("<key>UIAppFonts</key>"))
|
|
for font in Self.bundledFontFiles {
|
|
#expect(plist.contains("<string>\(font)</string>"))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func `extension text surfaces use branded typography helpers`() throws {
|
|
let activityTypeSource = try String(contentsOf: Self.activityWidgetTypographySourceURL(), encoding: .utf8)
|
|
let activitySource = try String(contentsOf: Self.activityWidgetSourceURL(), encoding: .utf8)
|
|
let watchTypeSource = try String(contentsOf: Self.watchTypographySourceURL(), encoding: .utf8)
|
|
let watchSource = try String(contentsOf: Self.watchInboxSourceURL(), encoding: .utf8)
|
|
|
|
#expect(activityTypeSource.contains("relativeTo: .subheadline"))
|
|
#expect(activityTypeSource.contains("relativeTo: .caption"))
|
|
#expect(!activityTypeSource.contains(".custom(\"RedHatDisplay-Regular\", size: size).weight"))
|
|
#expect(!activityTypeSource.contains(".custom(\"Inter-Regular\", size: size)"))
|
|
#expect(activitySource.contains("OpenClawActivityType.subheadSemiBold"))
|
|
#expect(activitySource.contains("OpenClawActivityType.subheadBold"))
|
|
#expect(activitySource.contains("OpenClawActivityType.caption"))
|
|
#expect(!activitySource.contains(".font(.subheadline"))
|
|
#expect(!activitySource.contains(".font(.caption"))
|
|
|
|
#expect(watchTypeSource.contains("relativeTo textStyle: Font.TextStyle"))
|
|
#expect(watchTypeSource.contains("body(size: 12, weight: .semibold, relativeTo: .caption)"))
|
|
#expect(watchTypeSource.contains("body(size: 12, weight: .bold, relativeTo: .caption)"))
|
|
#expect(watchTypeSource.contains("body(size: 11, relativeTo: .caption2)"))
|
|
#expect(watchTypeSource.contains("relativeTo: .caption2"))
|
|
#expect(watchTypeSource.contains("relativeTo: .headline"))
|
|
#expect(!watchTypeSource.contains(".custom(\"RedHatDisplay-Regular\", size: size).weight"))
|
|
#expect(!watchTypeSource.contains(".custom(\"Inter-Regular\", size: size).weight"))
|
|
#expect(watchSource.contains("WatchClawType.title"))
|
|
#expect(watchSource.contains("WatchClawType.body"))
|
|
#expect(watchSource.contains("WatchClawType.caption"))
|
|
#expect(!watchSource.contains(".font(.system"))
|
|
#expect(!watchSource.contains(".font(.caption"))
|
|
#expect(!watchSource.contains(".font(.title"))
|
|
}
|
|
|
|
@Test func `UIKit typography refreshes when Dynamic Type changes`() throws {
|
|
let appSource = try String(contentsOf: Self.appSourceURL(), encoding: .utf8)
|
|
let typographySource = try String(
|
|
contentsOf: Self.sourceURL("Design/OpenClawTypography.swift"),
|
|
encoding: .utf8)
|
|
|
|
#expect(appSource.contains("UIContentSizeCategory.didChangeNotification"))
|
|
#expect(appSource.contains("OpenClawType.refreshUIKitAppearance(in: Self.connectedWindows())"))
|
|
#expect(typographySource.contains("static func refreshUIKitAppearance(in windows: [UIWindow])"))
|
|
#expect(typographySource.contains("applyUIKitTypography(fonts, to: window)"))
|
|
#expect(typographySource.contains("maximumPointSize: 13"))
|
|
#expect(typographySource.contains("maximumPointSize: 16"))
|
|
#expect(typographySource.contains("maximumPointSize: 22"))
|
|
#expect(typographySource.contains("maximumPointSize: 44"))
|
|
#expect(typographySource.contains("scaledFont(for: base, maximumPointSize: maximumPointSize)"))
|
|
#expect(typographySource.contains("case let searchTextField as UISearchTextField"))
|
|
#expect(!typographySource.contains("case let textField as UITextField"))
|
|
#expect(!typographySource.contains("case let textView as UITextView"))
|
|
}
|
|
|
|
@Test func `listed iOS app surfaces enforce branded control typography`() throws {
|
|
let proComponents = try String(
|
|
contentsOf: Self.sourceURL("Design/OpenClawProComponents.swift"),
|
|
encoding: .utf8)
|
|
let quickSetup = try String(contentsOf: Self.sourceURL("Gateway/GatewayQuickSetupSheet.swift"), encoding: .utf8)
|
|
let gatewayProblem = try String(contentsOf: Self.sourceURL("Gateway/GatewayProblemView.swift"), encoding: .utf8)
|
|
let onboardingSteps = try String(
|
|
contentsOf: Self.sourceURL("Onboarding/OnboardingWizardSteps.swift"),
|
|
encoding: .utf8)
|
|
let onboardingWizard = try [
|
|
"Onboarding/OnboardingWizardView.swift",
|
|
"Onboarding/OnboardingWizardConnectionSections.swift",
|
|
"Onboarding/OnboardingWizardTypes.swift",
|
|
].map { path in
|
|
try String(contentsOf: Self.sourceURL(path), encoding: .utf8)
|
|
}.joined(separator: "\n")
|
|
let settingsSections = try String(
|
|
contentsOf: Self.sourceURL("Design/SettingsProTabSections.swift"),
|
|
encoding: .utf8)
|
|
let settingsSupport = try String(
|
|
contentsOf: Self.sourceURL("Design/SettingsProTabSupport.swift"),
|
|
encoding: .utf8)
|
|
let approvalDialog = try String(
|
|
contentsOf: Self.sourceURL("Gateway/ExecApprovalPromptDialog.swift"),
|
|
encoding: .utf8)
|
|
let privacyAccess = try String(
|
|
contentsOf: Self.sourceURL("Settings/PrivacyAccessSectionView.swift"),
|
|
encoding: .utf8)
|
|
let appleHealthAccess = try String(
|
|
contentsOf: Self.sourceURL("Health/AppleHealthAccessSectionView.swift"),
|
|
encoding: .utf8)
|
|
let skillWorkshop = try String(
|
|
contentsOf: Self.sourceURL("Design/IPadSkillWorkshopScreen.swift"),
|
|
encoding: .utf8)
|
|
let agentDestinations = try String(
|
|
contentsOf: Self.sourceURL("Design/AgentProTab+Destinations.swift"),
|
|
encoding: .utf8)
|
|
let dreaming = try String(
|
|
contentsOf: Self.sourceURL("Design/AgentProDreamingDestination.swift"),
|
|
encoding: .utf8)
|
|
let instances = try String(contentsOf: Self.sourceURL("Design/AgentProNodesDestination.swift"), encoding: .utf8)
|
|
let channels = try String(
|
|
contentsOf: Self.sourceURL("Design/SettingsChannelsDestination.swift"),
|
|
encoding: .utf8)
|
|
let skills = try String(
|
|
contentsOf: Self.sourceURL("Design/SettingsSkillsDestination.swift"),
|
|
encoding: .utf8)
|
|
let automations = try String(
|
|
contentsOf: Self.sourceURL("Design/AgentAutomationDetailScreen.swift"),
|
|
encoding: .utf8)
|
|
let docs = try String(contentsOf: Self.sourceURL("Design/OpenClawDocsScreen.swift"), encoding: .utf8)
|
|
let chatTab = try String(contentsOf: Self.sourceURL("Design/ChatProTab.swift"), encoding: .utf8)
|
|
let chatTypography = try String(
|
|
contentsOf: Self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatTypography.swift"),
|
|
encoding: .utf8)
|
|
let chatMessageViews = try String(
|
|
contentsOf: Self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift"),
|
|
encoding: .utf8)
|
|
let chatWorkingClawView = try String(
|
|
contentsOf: Self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatWorkingClawView.swift"),
|
|
encoding: .utf8)
|
|
let chatMarkdownRenderer = try String(
|
|
contentsOf: Self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift"),
|
|
encoding: .utf8)
|
|
|
|
#expect(automations.contains(".font(OpenClawType.body)"))
|
|
#expect(automations.contains(".font(OpenClawType.headline)"))
|
|
#expect(automations.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(automations.contains(".font(OpenClawType.caption)"))
|
|
#expect(!automations.contains(".font(.body"))
|
|
#expect(!automations.contains(".font(.headline"))
|
|
#expect(!automations.contains(".font(.caption"))
|
|
|
|
#expect(proComponents.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(proComponents.contains("primaryActionTitle.text"))
|
|
#expect(proComponents.contains("secondaryActionTitle.text"))
|
|
|
|
#expect(chatTab.contains("Text(\"Export Transcript\")"))
|
|
#expect(chatTab.contains("Text(String(localized: \"Sessions…\"))"))
|
|
#expect(chatTab.contains("Text(String(localized: \"Show reasoning & tool activity\"))"))
|
|
#expect(chatTab.contains(".font(OpenClawType.body)"))
|
|
#expect(!chatTab.contains("Button(\"Export Transcript\")"))
|
|
|
|
#expect(!quickSetup.contains("Button(\"Close\")"))
|
|
#expect(quickSetup.contains(".navigationTitle(\"Quick Setup\")"))
|
|
#expect(quickSetup.contains("Text(\"Close\")"))
|
|
#expect(quickSetup.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(quickSetup.contains("let text: LocalizedStringKey"))
|
|
#expect(!quickSetup.contains("Text(verbatim: self.text)"))
|
|
|
|
#expect(gatewayProblem.contains("Text(\"Connection problem\")"))
|
|
#expect(gatewayProblem.contains("Text(\"Copy request ID\")"))
|
|
#expect(gatewayProblem.contains("Text(\"Copy command\")"))
|
|
#expect(gatewayProblem.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
|
|
#expect(onboardingSteps.contains("title: \"Connect Gateway\""))
|
|
#expect(onboardingSteps.contains("Text(\"Scan QR\")"))
|
|
#expect(onboardingSteps.contains("Text(\"Connect Manually\")"))
|
|
#expect(onboardingSteps.contains("Label(\"Go to Chat\", systemImage: \"bubble.left.and.bubble.right.fill\")"))
|
|
#expect(onboardingSteps.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(onboardingSteps.contains("let title: LocalizedStringKey"))
|
|
#expect(onboardingSteps.contains("let subtitle: LocalizedStringKey?"))
|
|
#expect(onboardingSteps.contains("let text: LocalizedStringKey"))
|
|
|
|
#expect(onboardingWizard.contains("Text(\"Scan Setup Code\")")
|
|
|| onboardingWizard.contains(".navigationTitle(\"Scan Setup Code\")"))
|
|
#expect(onboardingWizard.contains("Label(\"Resume After Approval\", systemImage: \"arrow.clockwise\")"))
|
|
#expect(onboardingWizard.contains("Label(\"Scan Setup Code Again\", systemImage: \"qrcode.viewfinder\")"))
|
|
#expect(onboardingWizard.contains("Text(\"Apply\")"))
|
|
#expect(onboardingWizard.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(onboardingWizard.contains("_ title: LocalizedStringKey"))
|
|
#expect(onboardingWizard.contains("_ placeholder: LocalizedStringKey"))
|
|
#expect(onboardingWizard.contains("if self.developerModeEnabled.wrappedValue {"))
|
|
#expect(onboardingWizard.contains("title: \"Same Machine (Dev)\""))
|
|
#expect(onboardingWizard.contains("if lastMode == .developerLocal"))
|
|
#expect(onboardingWizard.contains("self.developerModeEnabled = true"))
|
|
let onboardingSecurityPicker = try Self.extract(
|
|
onboardingWizard,
|
|
from: "private var manualConnectionSecurityRows",
|
|
to: " private func onboardingLabeledContent")
|
|
let onboardingUnencryptedOption = try Self.extract(
|
|
onboardingSecurityPicker,
|
|
from: "Text(\"Unencrypted\")",
|
|
to: ".tag(false)")
|
|
let onboardingSecureOption = try Self.extract(
|
|
onboardingSecurityPicker,
|
|
from: "Text(\"Secure (TLS)\")",
|
|
to: ".tag(true)")
|
|
#expect(onboardingUnencryptedOption.contains(".font(OpenClawType.captionSemiBold)"))
|
|
#expect(onboardingSecureOption.contains(".font(OpenClawType.captionSemiBold)"))
|
|
|
|
#expect(settingsSections.contains(".font(OpenClawType.body)"))
|
|
#expect(settingsSections.contains("Text(warningText)"))
|
|
#expect(settingsSections.contains(".font(OpenClawType.caption)"))
|
|
#expect(approvalDialog.contains("Text(warningText)"))
|
|
#expect(approvalDialog.contains(".font(OpenClawType.footnote)"))
|
|
#expect(approvalDialog.contains("ScrollView {"))
|
|
#expect(approvalDialog.contains("self.actionFooter"))
|
|
#expect(approvalDialog.contains("exec-approval-review-scroll"))
|
|
#expect(approvalDialog.contains("exec-approval-actions"))
|
|
#expect(approvalDialog.contains("ViewThatFits(in: .horizontal)"))
|
|
#expect(settingsSections.contains("self.settingsToggle(\"Show Talk Control\", isOn: self.$talkButtonEnabled)"))
|
|
#expect(settingsSections.contains("OpenClawToggleIndicator(isOn: isOn.wrappedValue)"))
|
|
#expect(settingsSections.contains("TextField(\"Default Share Instruction\""))
|
|
#expect(settingsSections.contains(".font(OpenClawType.subhead)"))
|
|
#expect(settingsSections.contains("private struct AppearanceSettingsScreen"))
|
|
#expect(settingsSections.contains("Section(\"Gateway\")"))
|
|
#expect(settingsSections.contains("SettingsDetailRow(\"Address\", value: .verbatim(self.gatewayAddress))"))
|
|
#expect(settingsSections.contains("func gatewayActionButton"))
|
|
#expect(settingsSections.contains("func settingsToggle"))
|
|
#expect(settingsSections.contains(".font(OpenClawType.subheadSemiBold)"))
|
|
#expect(settingsSupport.contains("struct SettingsBuildMetadataStrip"))
|
|
#expect(settingsSupport.contains(".font(OpenClawType.caption2SemiBold)"))
|
|
#expect(settingsSupport.contains(".font(OpenClawType.monoSmall)"))
|
|
#expect(settingsSupport.contains("Text(\"Copy Build Info\")"))
|
|
#expect(settingsSections.contains("Text(\"Use Manual Gateway\")")
|
|
|| settingsSections.contains("\"Use Manual Gateway\""))
|
|
#expect(settingsSections.contains("func gatewaySecureField"))
|
|
#expect(settingsSections.contains("self.gatewaySecureField(\"Gateway Auth Token\""))
|
|
#expect(settingsSections.contains("self.gatewaySecureField(\"Gateway Password\""))
|
|
let gatewaySecureField = try Self.extract(
|
|
settingsSections,
|
|
from: "func gatewaySecureField",
|
|
to: " var voiceFeatureCard")
|
|
#expect(gatewaySecureField.contains(".accessibilityLabel(Text(placeholder))"))
|
|
#expect(gatewaySecureField.contains(".accessibilityHidden(true)"))
|
|
#expect(gatewaySecureField.contains(".textInputAutocapitalization(.never)"))
|
|
#expect(gatewaySecureField.contains(".autocorrectionDisabled()"))
|
|
#expect(settingsSections.contains("Picker(\"Default Agent\", selection: self.$selectedAgentPickerId)"))
|
|
#expect(settingsSections.contains("Text(\"Default\")"))
|
|
let settingsSecurityPicker = try Self.extract(
|
|
settingsSections,
|
|
from: "Picker(selection: self.manualGatewayTLSBinding)",
|
|
to: " .pickerStyle(.segmented)")
|
|
let settingsUnencryptedOption = try Self.extract(
|
|
settingsSecurityPicker,
|
|
from: "Text(\"Unencrypted\")",
|
|
to: ".tag(false)")
|
|
let settingsSecureOption = try Self.extract(
|
|
settingsSecurityPicker,
|
|
from: "Text(\"Secure (TLS)\")",
|
|
to: ".tag(true)")
|
|
#expect(settingsUnencryptedOption.contains(".font(OpenClawType.captionSemiBold)"))
|
|
#expect(settingsSecureOption.contains(".font(OpenClawType.captionSemiBold)"))
|
|
|
|
#expect(!privacyAccess.contains("DisclosureGroup(\"Privacy & Access\")"))
|
|
#expect(privacyAccess.contains("Text(\"Privacy & Access\")"))
|
|
#expect(appleHealthAccess.contains("Text(healthError)"))
|
|
#expect(appleHealthAccess.contains(".font(OpenClawType.footnote)"))
|
|
let permissionRow = try String(
|
|
contentsOf: Self.sourceURL("Permissions/DevicePermissionRow.swift"),
|
|
encoding: .utf8)
|
|
#expect(permissionRow.contains("Text(actionTitle)"))
|
|
#expect(permissionRow.contains(".font(OpenClawType.footnoteSemiBold)"))
|
|
|
|
#expect(!skillWorkshop.contains("Button(\"Done\")"))
|
|
#expect(skillWorkshop.contains("Label(\"Refresh\", systemImage: \"arrow.clockwise\")"))
|
|
#expect(skillWorkshop.contains("Text(\"Default agent\")"))
|
|
#expect(skillWorkshop.contains("Text(\"Inspect\")"))
|
|
#expect(skillWorkshop.contains("Text(\"Apply\")"))
|
|
#expect(skillWorkshop.contains("Text(\"Reject\")"))
|
|
|
|
#expect(skills.contains("Text(\"Gateway warning\").font(OpenClawType.headline)"))
|
|
#expect(skills.contains("Text(\"Acknowledge and install\").font(OpenClawType.subheadSemiBold)"))
|
|
#expect(skills.contains("prompt: Text(\"Search ClawHub\").font(OpenClawType.body)"))
|
|
|
|
for source in [agentDestinations, dreaming, instances, channels, skills, docs] {
|
|
#expect(source.contains(".font(OpenClawType.body)"))
|
|
}
|
|
|
|
#expect(chatMessageViews.contains("typography: segment.kind.markdownTypography"))
|
|
#expect(chatMessageViews.contains(".font(OpenClawChatTypography.caption)"))
|
|
#expect(chatMessageViews.contains("chat-user-message-disclosure-toggle"))
|
|
#expect(chatMarkdownRenderer.contains("OpenClawChatTypography.callout.italic()"))
|
|
#expect(!chatMessageViews.contains("font: .body"))
|
|
#expect(!chatMessageViews.contains("Font.body"))
|
|
#expect(!chatMessageViews.contains("Font.callout"))
|
|
#expect(chatWorkingClawView.contains(".font(OpenClawChatTypography.caption)"))
|
|
#expect(chatWorkingClawView.contains(".font(OpenClawChatTypography.captionSemiBold)"))
|
|
#expect(!chatWorkingClawView.contains(".font(."))
|
|
#expect(chatMarkdownRenderer.contains(".font(self.typography.proseFont)"))
|
|
#expect(chatTypography
|
|
.contains("Font.custom(self.macSystemFontName(size: size), size: size, relativeTo: textStyle)"))
|
|
#expect(chatTypography.contains(
|
|
"Font.custom(self.macMonospacedSystemFontName(size: size), size: size, relativeTo: textStyle)"))
|
|
#expect(!chatTypography.contains("Font.system(textStyle, design: .default)"))
|
|
#expect(!chatTypography.contains("Font.system(textStyle, design: .monospaced)"))
|
|
}
|
|
|
|
@Test func `iOS app text and control calls keep branded font boundaries`() throws {
|
|
let offenders = try Self.unbrandedTextCallOffenders()
|
|
#expect(offenders.isEmpty, Comment(rawValue: offenders.joined(separator: "\n")))
|
|
}
|
|
|
|
@Test func `font boundary scanner permits inheritance and rejects system overrides`() {
|
|
let source = """
|
|
Text("Inherited")
|
|
Text("Branded")
|
|
.padding()
|
|
.font(OpenClawType.body)
|
|
Text("System")
|
|
.padding()
|
|
.font(.body)
|
|
Button("System") {}
|
|
.font(.headline)
|
|
VStack {
|
|
Text("Inherited system")
|
|
}
|
|
.font(.body)
|
|
Text("Trailing closure")
|
|
.background { Color.clear }
|
|
.font(.caption)
|
|
Text("Later override")
|
|
.font(OpenClawType.body)
|
|
.font(.title)
|
|
NavigationStack {
|
|
Text("Custom boundary")
|
|
}
|
|
.font(.footnote)
|
|
Button {
|
|
action()
|
|
} label: {
|
|
Text("Closure-only control")
|
|
}
|
|
.font(.headline)
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 14))
|
|
"""
|
|
|
|
let offenders = Self.unbrandedTextCallOffenders(in: source, relativePath: "Sample.swift")
|
|
#expect(offenders.count == 7)
|
|
#expect(offenders[0].contains(".font(.body)"))
|
|
#expect(offenders[1].contains(".font(.headline)"))
|
|
#expect(offenders[2].contains(".font(.body)"))
|
|
#expect(offenders[3].contains(".font(.caption)"))
|
|
#expect(offenders[4].contains(".font(.title)"))
|
|
#expect(offenders[5].contains(".font(.footnote)"))
|
|
#expect(offenders[6].contains(".font(.headline)"))
|
|
}
|
|
|
|
@Test func `accessibility metadata text does not require visual typography`() throws {
|
|
let accessibilityTextSamples = [
|
|
".accessibilityLabel(Text(title))",
|
|
"Image(systemName: \"circle\").accessibilityLabel(Text(title))",
|
|
".accessibilityLabel(Text(title)).accessibilityIdentifier(\"status\")",
|
|
"Image(systemName: \"circle\").accessibilityLabel(Text(title)).accessibilityHint(Text(hint))",
|
|
".accessibilityLabel(Text(self.statusLabel ?? LocalizedStringResource(\"Allowed\")))",
|
|
".accessibilityLabel(Text(title))\n)",
|
|
".accessibilityLabel(\n Text(title)\n)",
|
|
"Image(systemName: \"circle\")\n .accessibilityLabel(\n Text(title)\n )",
|
|
".accessibilityValue(\n Text(value))",
|
|
".accessibilityHint(\n\n Text(hint))",
|
|
]
|
|
for source in accessibilityTextSamples {
|
|
let lines = source.components(separatedBy: .newlines)
|
|
let idx = try #require(lines.firstIndex { Self.isTextOrLabelCall($0) })
|
|
#expect(Self.isAccessibilityMetadataTextCall(at: idx, in: lines))
|
|
}
|
|
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 0, in: ["Text(title)"]))
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 0, in: [".accessibilityLabel(title)"]))
|
|
|
|
let nearbyVisualText = [
|
|
".accessibilityLabel(Text(title))",
|
|
"Text(body)",
|
|
]
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 1, in: nearbyVisualText))
|
|
|
|
let sameLineVisualLabel = [
|
|
"Label(\"Status\", systemImage: \"circle\").accessibilityLabel(Text(status))",
|
|
]
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 0, in: sameLineVisualLabel))
|
|
|
|
let trailingVisualText = [".accessibilityLabel(Text(status)); Text(body)"]
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 0, in: trailingVisualText))
|
|
|
|
let modifierTextInString = [
|
|
"let sample = \".accessibilityLabel(\"",
|
|
"Text(body)",
|
|
]
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 1, in: modifierTextInString))
|
|
|
|
let modifierTextInComment = [
|
|
"// Example: .accessibilityLabel(",
|
|
"Text(body)",
|
|
]
|
|
#expect(!Self.isAccessibilityMetadataTextCall(at: 1, in: modifierTextInComment))
|
|
}
|
|
|
|
@Test func `secure fields do not use platform placeholder text`() throws {
|
|
let offenders = try Self.swiftSourcesForTypographyAudit().flatMap { url -> [String] in
|
|
let source = try String(contentsOf: url, encoding: .utf8)
|
|
return source
|
|
.components(separatedBy: .newlines)
|
|
.enumerated()
|
|
.compactMap { offset, line in
|
|
guard line.range(
|
|
of: #"\bSecureField\("[^"]+""#,
|
|
options: .regularExpression) != nil
|
|
else { return nil }
|
|
return "\(Self.relativePath(url)):\(offset + 1): \(line.trimmingCharacters(in: .whitespaces))"
|
|
}
|
|
}
|
|
#expect(offenders.isEmpty, Comment(rawValue: offenders.joined(separator: "\n")))
|
|
}
|
|
|
|
private static let bundledFontFiles = [
|
|
"RedHatDisplay[wght].ttf",
|
|
"Inter[opsz,wght].ttf",
|
|
"Inter-Italic[opsz,wght].ttf",
|
|
"JetBrainsMono-Regular.ttf",
|
|
"JetBrainsMono-Medium.ttf",
|
|
"JetBrainsMono-SemiBold.ttf",
|
|
]
|
|
|
|
private static func projectYmlURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("project.yml")
|
|
}
|
|
|
|
private static func activityWidgetInfoPlistURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("ActivityWidget/Info.plist")
|
|
}
|
|
|
|
private static func watchInfoPlistURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("WatchApp/Info.plist")
|
|
}
|
|
|
|
private static func activityWidgetSourceURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("ActivityWidget/OpenClawLiveActivity.swift")
|
|
}
|
|
|
|
private static func activityWidgetTypographySourceURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("ActivityWidget/OpenClawActivityTypography.swift")
|
|
}
|
|
|
|
private static func watchInboxSourceURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("WatchApp/Sources/WatchInboxView.swift")
|
|
}
|
|
|
|
private static func watchTypographySourceURL() -> URL {
|
|
self.iosRootURL().appendingPathComponent("WatchApp/Sources/WatchClawTypography.swift")
|
|
}
|
|
|
|
private static func appSourceURL() -> URL {
|
|
self.sourceURL("OpenClawApp.swift")
|
|
}
|
|
|
|
private static func sourceURL(_ relativePath: String) -> URL {
|
|
self.iosRootURL().appendingPathComponent("Sources/\(relativePath)")
|
|
}
|
|
|
|
private static func swiftSourcesForTypographyAudit() throws -> [URL] {
|
|
let roots = [
|
|
self.sourceURL(""),
|
|
self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.appendingPathComponent("shared/OpenClawKit/Sources/OpenClawChatUI"),
|
|
]
|
|
return roots.flatMap { root -> [URL] in
|
|
guard let enumerator = FileManager.default.enumerator(
|
|
at: root,
|
|
includingPropertiesForKeys: nil)
|
|
else { return [] }
|
|
return enumerator.compactMap { item -> URL? in
|
|
guard let url = item as? URL, url.pathExtension == "swift" else { return nil }
|
|
return url
|
|
}
|
|
}
|
|
.sorted { $0.path < $1.path }
|
|
}
|
|
|
|
private static func unbrandedTextCallOffenders() throws -> [String] {
|
|
try self.swiftSourcesForTypographyAudit().flatMap { url -> [String] in
|
|
let source = try String(contentsOf: url, encoding: .utf8)
|
|
return self.unbrandedTextCallOffenders(in: source, relativePath: self.relativePath(url))
|
|
}
|
|
}
|
|
|
|
private static func unbrandedTextCallOffenders(in source: String, relativePath: String) -> [String] {
|
|
let fontTokens = ["OpenClawType", "OpenClawChatTypography", "typography."]
|
|
let sourceBytes = Array(source.utf8)
|
|
let code = self.maskedSwiftCode(source)
|
|
let imageFontRanges = Set(self.directImageFontModifierRanges(in: code))
|
|
var offenders: [String] = []
|
|
|
|
for fontRange in self.fontModifierRanges(in: code) {
|
|
let fontCall = String(decoding: sourceBytes[fontRange], as: UTF8.self)
|
|
let hasBrandedFont = fontTokens.contains { fontCall.contains($0) }
|
|
|| self.hasAllowedBrandedFontParameter(fontCall, relativePath: relativePath)
|
|
guard !hasBrandedFont, !imageFontRanges.contains(fontRange) else { continue }
|
|
|
|
let fontLine = code[..<fontRange.lowerBound].count(where: { $0 == 10 })
|
|
offenders.append(
|
|
"\(relativePath):\(fontLine + 1): \(fontCall.trimmingCharacters(in: .whitespacesAndNewlines))")
|
|
}
|
|
return offenders
|
|
}
|
|
|
|
private static let textOrLabelCallPattern = try! NSRegularExpression(pattern: #"\b(Text|Label)\s*\("#)
|
|
|
|
private static func isTextOrLabelCall(_ line: String) -> Bool {
|
|
self.textOrLabelCallPattern.firstMatch(
|
|
in: line,
|
|
range: NSRange(line.startIndex..<line.endIndex, in: line)) != nil
|
|
}
|
|
|
|
private static func isAccessibilityMetadataTextCall(at idx: Int, in lines: [String]) -> Bool {
|
|
self.accessibilityMetadataTextLines(in: lines).contains(idx)
|
|
}
|
|
|
|
private static func accessibilityMetadataTextLines(in lines: [String]) -> Set<Int> {
|
|
let code = self.maskedSwiftCode(lines.joined(separator: "\n"))
|
|
let textToken = Array("Text".utf8)
|
|
let labelToken = Array("Label".utf8)
|
|
let modifiers = [
|
|
Array(".accessibilityLabel".utf8),
|
|
Array(".accessibilityValue".utf8),
|
|
Array(".accessibilityHint".utf8),
|
|
]
|
|
var callCounts: [Int: Int] = [:]
|
|
var metadataCallCounts: [Int: Int] = [:]
|
|
var line = 0
|
|
|
|
for offset in code.indices {
|
|
if code[offset] == 10 {
|
|
line += 1
|
|
continue
|
|
}
|
|
if self.callOpeningParenthesis(after: textToken, at: offset, in: code) != nil ||
|
|
self.callOpeningParenthesis(after: labelToken, at: offset, in: code) != nil
|
|
{
|
|
callCounts[line, default: 0] += 1
|
|
}
|
|
|
|
for modifier in modifiers {
|
|
guard let modifierOpen = self.callOpeningParenthesis(after: modifier, at: offset, in: code)
|
|
else { continue }
|
|
let textOffset = self.skippingWhitespace(in: code, after: modifierOpen)
|
|
guard let textOpen = self.callOpeningParenthesis(after: textToken, at: textOffset, in: code),
|
|
let textClose = self.matchingParenthesis(in: code, openingAt: textOpen)
|
|
else { continue }
|
|
let metadataClose = self.skippingWhitespace(in: code, after: textClose)
|
|
guard metadataClose < code.count, code[metadataClose] == 41 else { continue }
|
|
|
|
// Exempt only the direct Text argument; visible calls sharing its source line stay audited.
|
|
let textLine = line + code[offset..<textOffset].count(where: { $0 == 10 })
|
|
metadataCallCounts[textLine, default: 0] += 1
|
|
}
|
|
}
|
|
|
|
return Set(metadataCallCounts.compactMap { line, count in
|
|
callCounts[line] == count ? line : nil
|
|
})
|
|
}
|
|
|
|
private static func callOpeningParenthesis(
|
|
after token: [UInt8],
|
|
at offset: Int,
|
|
in code: [UInt8]) -> Int?
|
|
{
|
|
guard let firstByte = token.first,
|
|
offset + token.count <= code.count,
|
|
code[offset] == firstByte,
|
|
code[offset..<(offset + token.count)].elementsEqual(token),
|
|
firstByte == 46 || offset == 0 || !self.isSwiftIdentifierByte(code[offset - 1])
|
|
else { return nil }
|
|
let afterToken = offset + token.count
|
|
guard afterToken == code.count || !self.isSwiftIdentifierByte(code[afterToken]) else { return nil }
|
|
let opening = self.skippingWhitespace(in: code, after: afterToken - 1)
|
|
return opening < code.count && code[opening] == 40 ? opening : nil
|
|
}
|
|
|
|
private static func skippingWhitespace(in code: [UInt8], after offset: Int) -> Int {
|
|
var cursor = offset + 1
|
|
while cursor < code.count, code[cursor] == 9 || code[cursor] == 10 || code[cursor] == 13 || code[cursor] == 32 {
|
|
cursor += 1
|
|
}
|
|
return cursor
|
|
}
|
|
|
|
private static func matchingParenthesis(in code: [UInt8], openingAt offset: Int) -> Int? {
|
|
self.matchingDelimiter(in: code, openingAt: offset, opening: 40, closing: 41)
|
|
}
|
|
|
|
private static func matchingDelimiter(
|
|
in code: [UInt8],
|
|
openingAt offset: Int,
|
|
opening: UInt8,
|
|
closing: UInt8) -> Int?
|
|
{
|
|
var depth = 0
|
|
for cursor in offset..<code.count {
|
|
if code[cursor] == opening {
|
|
depth += 1
|
|
} else if code[cursor] == closing {
|
|
depth -= 1
|
|
if depth == 0 { return cursor }
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func isSwiftIdentifierByte(_ byte: UInt8) -> Bool {
|
|
byte == 95 || (48...57).contains(byte) || (65...90).contains(byte) || (97...122).contains(byte) || byte >= 128
|
|
}
|
|
|
|
private static func maskedSwiftCode(_ source: String) -> [UInt8] {
|
|
let sourceBytes = Array(source.utf8)
|
|
var code = sourceBytes
|
|
var cursor = 0
|
|
|
|
func mask(_ range: Range<Int>) {
|
|
for offset in range where code[offset] != 10 && code[offset] != 13 {
|
|
code[offset] = 32
|
|
}
|
|
}
|
|
|
|
while cursor < sourceBytes.count {
|
|
if cursor + 1 < sourceBytes.count, sourceBytes[cursor] == 47, sourceBytes[cursor + 1] == 47 {
|
|
let start = cursor
|
|
while cursor < sourceBytes.count, sourceBytes[cursor] != 10 {
|
|
cursor += 1
|
|
}
|
|
mask(start..<cursor)
|
|
continue
|
|
}
|
|
if cursor + 1 < sourceBytes.count, sourceBytes[cursor] == 47, sourceBytes[cursor + 1] == 42 {
|
|
let start = cursor
|
|
var depth = 1
|
|
cursor += 2
|
|
while cursor < sourceBytes.count, depth > 0 {
|
|
if cursor + 1 < sourceBytes.count, sourceBytes[cursor] == 47, sourceBytes[cursor + 1] == 42 {
|
|
depth += 1
|
|
cursor += 2
|
|
} else if cursor + 1 < sourceBytes.count,
|
|
sourceBytes[cursor] == 42,
|
|
sourceBytes[cursor + 1] == 47
|
|
{
|
|
depth -= 1
|
|
cursor += 2
|
|
} else {
|
|
cursor += 1
|
|
}
|
|
}
|
|
mask(start..<cursor)
|
|
continue
|
|
}
|
|
|
|
let start = cursor
|
|
var hashCount = 0
|
|
while cursor < sourceBytes.count, sourceBytes[cursor] == 35 {
|
|
hashCount += 1
|
|
cursor += 1
|
|
}
|
|
guard cursor < sourceBytes.count, sourceBytes[cursor] == 34 else {
|
|
cursor = start + 1
|
|
continue
|
|
}
|
|
let quoteCount = cursor + 2 < sourceBytes.count &&
|
|
sourceBytes[cursor + 1] == 34 && sourceBytes[cursor + 2] == 34 ? 3 : 1
|
|
cursor += quoteCount
|
|
while cursor < sourceBytes.count {
|
|
if hashCount == 0, quoteCount == 1, sourceBytes[cursor] == 92 {
|
|
cursor = min(sourceBytes.count, cursor + 2)
|
|
continue
|
|
}
|
|
let closingEnd = cursor + quoteCount + hashCount
|
|
if closingEnd <= sourceBytes.count,
|
|
sourceBytes[cursor..<(cursor + quoteCount)].allSatisfy({ $0 == 34 }),
|
|
sourceBytes[(cursor + quoteCount)..<closingEnd].allSatisfy({ $0 == 35 })
|
|
{
|
|
cursor = closingEnd
|
|
break
|
|
}
|
|
cursor += 1
|
|
}
|
|
mask(start..<cursor)
|
|
}
|
|
return code
|
|
}
|
|
|
|
private static func fontModifierRanges(in code: [UInt8]) -> [Range<Int>] {
|
|
let token = Array(".font".utf8)
|
|
return code.indices.compactMap { offset in
|
|
guard let opening = self.callOpeningParenthesis(after: token, at: offset, in: code),
|
|
let closing = self.matchingParenthesis(in: code, openingAt: opening)
|
|
else { return nil }
|
|
return offset..<(closing + 1)
|
|
}
|
|
}
|
|
|
|
private static func directImageFontModifierRanges(in code: [UInt8]) -> [Range<Int>] {
|
|
let imageToken = Array("Image".utf8)
|
|
let fontToken = Array(".font".utf8)
|
|
return code.indices.compactMap { offset in
|
|
guard let imageOpening = self.callOpeningParenthesis(after: imageToken, at: offset, in: code),
|
|
let imageClosing = self.matchingParenthesis(in: code, openingAt: imageOpening)
|
|
else { return nil }
|
|
let fontStart = self.skippingWhitespace(in: code, from: imageClosing + 1)
|
|
guard let fontOpening = self.callOpeningParenthesis(after: fontToken, at: fontStart, in: code),
|
|
let fontClosing = self.matchingParenthesis(in: code, openingAt: fontOpening)
|
|
else { return nil }
|
|
return fontStart..<(fontClosing + 1)
|
|
}
|
|
}
|
|
|
|
private static func skippingWhitespace(in code: [UInt8], from offset: Int) -> Int {
|
|
var cursor = offset
|
|
while cursor < code.count, code[cursor] == 9 || code[cursor] == 10 || code[cursor] == 13 || code[cursor] == 32 {
|
|
cursor += 1
|
|
}
|
|
return cursor
|
|
}
|
|
|
|
private static func hasAllowedBrandedFontParameter(_ fontCall: String, relativePath: String) -> Bool {
|
|
switch relativePath {
|
|
case "apps/ios/Sources/Design/OpenClawProComponents.swift":
|
|
fontCall.contains(".font(self.titleFont)") ||
|
|
fontCall.contains(".font(self.subtitleFont)")
|
|
case "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownRenderer.swift":
|
|
// Qualified values are composed here, then styled at the prose render boundary.
|
|
fontCall.contains(".font(self.font)") || fontCall.contains(".font(font)")
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
private static func relativePath(_ url: URL) -> String {
|
|
let rootPath = self.iosRootURL()
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
.path + "/"
|
|
return url.path.hasPrefix(rootPath) ? String(url.path.dropFirst(rootPath.count)) : url.path
|
|
}
|
|
|
|
private static func iosRootURL() -> URL {
|
|
URL(fileURLWithPath: #filePath)
|
|
.deletingLastPathComponent()
|
|
.deletingLastPathComponent()
|
|
}
|
|
|
|
private static func extract(_ source: String, from start: String, to end: String) throws -> String {
|
|
let startRange = try #require(source.range(of: start))
|
|
let tail = source[startRange.lowerBound...]
|
|
let endRange = try #require(tail.range(of: end))
|
|
return String(tail[..<endRange.lowerBound])
|
|
}
|
|
}
|