Files
OSGKeyboard/OSGKeyboardTests/HomeAccountSurfacePolicyTests.swift
Rocky 4dce67df84 chore(release): bump version to 2.1.6 (build 131)
Version: CURRENT_PROJECT_VERSION 130 -> 131 (project.yml, AGENTS.md, READMEs,
docs/llms.txt).

fix(flow): host-app identification now compiles in every build configuration
(Debug, TestFlight, Release). The arbiter probe and HostAppIdentity were
DEBUG-only, so TestFlight/App Store builds never knew the host bundle id and
could not auto-return after a startflow handoff. Debug-only logging and the
Home test card are unchanged.

Also includes the pending work in the tree:
- Home: single account/setup card surface (HomeAccountSurfacePolicy/Views)
- Wide screens: voice key row and centred mic placement, iPhone Duo layout
- Keyboard: clipboard skill ranking and AI skill layout updates
- Debug: account session handoff to the Simulator (+ import script)
- Docs: portfolio assets, privacy policy URL, site/README refresh
2026-10-29 11:54:13 +08:00

170 lines
6.3 KiB
Swift

// HomeAccountSurfacePolicyTests.swift
// OSGKeyboardTests
@testable import OSGKeyboard
import OSGKeyboardShared
import XCTest
final class HomeAccountSurfacePolicyTests: XCTestCase {
// MARK: - Readiness
func testCreditsNeedAnAccountButSurviveRestore() {
XCTAssertEqual(readiness(.managed, account: .signedIn), .credits)
XCTAssertEqual(readiness(.managed, account: .restoring), .credits)
XCTAssertEqual(readiness(.managed, account: .signedOut), .unavailable)
}
func testOwnKeyReadinessFollowsConfiguredKeys() {
XCTAssertEqual(readiness(.byok, asr: true, polish: true), .ownKey)
XCTAssertEqual(readiness(.byok, asr: true, polish: false), .dictationOnly)
XCTAssertEqual(readiness(.byok, asr: false, polish: true), .unavailable)
}
// MARK: - Surface rules, in priority order
func testMissingPermissionsOutrankEverything() {
XCTAssertEqual(
surface(needsPermissionSetup: true, account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
.permissions
)
}
func testRestoringShowsNoAccountSurface() {
XCTAssertNil(surface(account: .restoring, readiness: .dictationOnly))
}
func testSavedInvitationLeadsUntilDeferred() {
XCTAssertEqual(
surface(account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
.invitation(code: "K7M2Q9XA")
)
XCTAssertEqual(
surface(
account: .signedOut,
pendingReferralCode: "K7M2Q9XA",
isInvitationDeferred: true,
readiness: .dictationOnly
),
.signInSetup
)
}
func testSignedOutWithoutWorkingServiceAsksToSignIn() {
XCTAssertEqual(surface(account: .signedOut, readiness: .dictationOnly), .signInSetup)
XCTAssertEqual(surface(account: .signedOut, readiness: .unavailable), .signInSetup)
XCTAssertEqual(
surface(account: .signedOut, credentialSource: .managed, readiness: .unavailable),
.signInSetup
)
}
func testSignedOutWithWorkingKeysOnlyGetsQuietPromo() {
XCTAssertEqual(surface(account: .signedOut, readiness: .ownKey), .signInPromo)
}
func testCreditsConnectionFailureBecomesProblemCard() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .failed),
.creditsUnavailable
)
}
func testDepletedCreditsBecomeProblemCard() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(0)),
.creditsDepleted
)
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(-5)),
.creditsDepleted
)
}
func testHealthyOrLoadingCreditsStayQuiet() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(1_500)),
.accountSummary
)
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .loading),
.accountSummary
)
}
func testSignedInWithoutKeysChoosesService() {
XCTAssertEqual(surface(account: .signedIn, readiness: .dictationOnly), .chooseService)
XCTAssertEqual(surface(account: .signedIn, readiness: .unavailable), .chooseService)
}
func testSignedInWithOwnKeysIgnoresCreditErrors() {
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .failed), .accountSummary)
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .balance(0)), .accountSummary)
}
func testSignedInIgnoresStalePendingInvitation() {
XCTAssertEqual(
surface(account: .signedIn, pendingReferralCode: "K7M2Q9XA", readiness: .ownKey),
.accountSummary
)
}
// MARK: - Layout
func testUnfoldedDuoUsesWideLayoutEvenWithCompactSizeClass() {
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 669))
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 466))
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 402))
}
func testRegularSizeClassKeepsWideLayoutInsideSplitView() {
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: true, containerWidth: 504))
}
func testWideTopClearanceOnlyWhereNoStatusBarSitsAbove() {
// Landscape Duo inner display: status bar on the side, nothing above.
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 0), Spacing.md)
// iPad status bar, portrait Duo inner display (measured 82 pt).
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 24), 0)
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 82), 0)
// A thin inset only gets topped up to the minimum.
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 6), Spacing.md - 6)
}
// MARK: - Helpers
private func readiness(
_ source: CredentialSource,
account: HomeAccountPhase = .signedIn,
asr: Bool = true,
polish: Bool = true
) -> HomeServiceReadiness {
HomeAccountSurfacePolicy.readiness(
credentialSource: source,
account: account,
isASRConfigured: asr,
isPolishConfigured: polish
)
}
private func surface(
needsPermissionSetup: Bool = false,
account: HomeAccountPhase,
pendingReferralCode: String? = nil,
isInvitationDeferred: Bool = false,
credentialSource: CredentialSource = .byok,
readiness: HomeServiceReadiness = .ownKey,
credits: HomeCreditsState = .loading
) -> HomeAccountSurface? {
HomeAccountSurfacePolicy.surface(
needsPermissionSetup: needsPermissionSetup,
account: account,
pendingReferralCode: pendingReferralCode,
isInvitationDeferred: isInvitationDeferred,
credentialSource: credentialSource,
readiness: readiness,
credits: credits
)
}
}