feat: TypeWhisper Flow sessions, Phase 4 UX, and GitHub Pages privacy site
Migrate keyboard dictation to continuous Flow sessions with auto-start, tap-to-toggle recording, 60s countdown, five-step onboarding, and App Group IPC. Add docs/ GitHub Pages site with en/zh privacy policy for App Store compliance.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// DictationBridgeTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class DictationBridgeTests: XCTestCase {
|
||||
|
||||
private func makeDefaults() -> UserDefaults {
|
||||
let suite = "group.com.osgkeyboard.shared.tests.dictation.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
return defaults
|
||||
}
|
||||
|
||||
func testStoreAndConsumeTranscript() {
|
||||
let defaults = makeDefaults()
|
||||
|
||||
DictationBridge.storePendingTranscript(" hello ", defaults: defaults)
|
||||
let consumed = DictationBridge.consumePendingTranscript(defaults: defaults)
|
||||
|
||||
XCTAssertEqual(consumed, "hello")
|
||||
XCTAssertNil(DictationBridge.consumePendingTranscript(defaults: defaults))
|
||||
}
|
||||
|
||||
func testConsumeIgnoresExpiredTranscript() {
|
||||
let defaults = makeDefaults()
|
||||
DictationBridge.storePendingTranscript("stale", defaults: defaults)
|
||||
// maxAge = 1ms, then delay to force expiry
|
||||
usleep(2_000)
|
||||
let consumed = DictationBridge.consumePendingTranscript(maxAge: 0.001, defaults: defaults)
|
||||
XCTAssertNil(consumed)
|
||||
}
|
||||
|
||||
func testStatusLifecycle() {
|
||||
let defaults = makeDefaults()
|
||||
|
||||
DictationBridge.markRequested(defaults: defaults)
|
||||
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .requested)
|
||||
|
||||
DictationBridge.setStatus(.recording, defaults: defaults)
|
||||
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .recording)
|
||||
|
||||
DictationBridge.storePendingTranscript("ok", defaults: defaults)
|
||||
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .done)
|
||||
|
||||
_ = DictationBridge.consumePendingTranscript(defaults: defaults)
|
||||
XCTAssertEqual(DictationBridge.currentStatus(defaults: defaults).status, .idle)
|
||||
}
|
||||
|
||||
func testStatusMessageAndTimestamp() {
|
||||
let defaults = makeDefaults()
|
||||
DictationBridge.setStatus(.error, message: "fail", defaults: defaults)
|
||||
let snapshot = DictationBridge.currentStatus(defaults: defaults)
|
||||
XCTAssertEqual(snapshot.status, .error)
|
||||
XCTAssertEqual(snapshot.message, "fail")
|
||||
XCTAssertGreaterThan(snapshot.updatedAt, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// FlowSessionBridgeTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class FlowSessionBridgeTests: XCTestCase {
|
||||
|
||||
private func makeDefaults() -> UserDefaults {
|
||||
let suite = "group.com.osgkeyboard.shared.tests.flow.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defaults.removePersistentDomain(forName: suite)
|
||||
return defaults
|
||||
}
|
||||
|
||||
func testSessionActiveRequiresFreshHeartbeat() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(duration: 60, defaults: defaults)
|
||||
XCTAssertTrue(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
|
||||
let staleHeartbeat = Date().timeIntervalSince1970 - 10
|
||||
defaults.set(staleHeartbeat, forKey: FlowSessionKeys.flowHeartbeat)
|
||||
XCTAssertFalse(FlowSessionBridge.isSessionActive(defaults: defaults))
|
||||
}
|
||||
|
||||
func testRecordingStateRoundTrip() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.setRecordingState(.recording, defaults: defaults)
|
||||
XCTAssertEqual(FlowSessionBridge.recordingState(defaults: defaults), .recording)
|
||||
|
||||
FlowSessionBridge.setRecordingState(.stopped, defaults: defaults)
|
||||
XCTAssertEqual(FlowSessionBridge.recordingState(defaults: defaults), .stopped)
|
||||
}
|
||||
|
||||
func testConsumeTranscriptionResultClearsKey() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.storeTranscriptionResult("hello", defaults: defaults)
|
||||
XCTAssertEqual(FlowSessionBridge.consumeTranscriptionResult(defaults: defaults), "hello")
|
||||
XCTAssertNil(FlowSessionBridge.consumeTranscriptionResult(defaults: defaults))
|
||||
}
|
||||
|
||||
func testClearFlowStateRemovesSessionKeys() {
|
||||
let defaults = makeDefaults()
|
||||
FlowSessionBridge.markSessionActive(defaults: defaults)
|
||||
FlowSessionBridge.storeTranscriptionResult("x", defaults: defaults)
|
||||
FlowSessionBridge.clearFlowState(defaults: defaults)
|
||||
|
||||
XCTAssertFalse(defaults.bool(forKey: FlowSessionKeys.flowSessionActive))
|
||||
XCTAssertNil(FlowSessionBridge.consumeTranscriptionResult(defaults: defaults))
|
||||
XCTAssertEqual(FlowSessionBridge.recordingState(defaults: defaults), .idle)
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,7 @@
|
||||
// the ASR to never yield `.final` and asserts the timeout fires.
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboard
|
||||
import AVFoundation
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
@MainActor
|
||||
final class PreviewASRControllerStateTests: XCTestCase {
|
||||
@@ -41,7 +40,7 @@ final class PreviewASRControllerStateTests: XCTestCase {
|
||||
/// is the regression test for the original bug — if it ever
|
||||
/// fails, the disc is stuck again.
|
||||
func testStopDoesNotCancelConsumerTask() {
|
||||
let controller = PreviewASRController()
|
||||
let controller = LiveDictationController()
|
||||
let consumerTask = Task<Void, Never> {}
|
||||
controller.asrTask = consumerTask
|
||||
|
||||
@@ -58,7 +57,7 @@ final class PreviewASRControllerStateTests: XCTestCase {
|
||||
/// misbehave — no double-cancel, no extra state transitions.
|
||||
/// This is the contract `.onDisappear` relies on.
|
||||
func testStopIsIdempotent() {
|
||||
let controller = PreviewASRController()
|
||||
let controller = LiveDictationController()
|
||||
let consumerTask = Task<Void, Never> {}
|
||||
controller.asrTask = consumerTask
|
||||
|
||||
|
||||
Reference in New Issue
Block a user