Files
buzz/mobile/ios/NativeControlTests/MeAgentAccessibilityTests.swift
kenny lopez ad99b13f46 fix(mobile): persist Solo retries and show their saved recipient
Rewrite the recovery journal after failed cached preference writes. Render the saved agent identity in locked native retries even when discovery has not returned it.

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
2026-10-26 20:09:38 +00:00

174 lines
9.0 KiB
Swift

import Flutter
import UIKit
@main
struct MeAgentAccessibilityTests {
@MainActor static func main() {
for locked in [false, true] {
for empty in [false, true] {
let savedName = "Saved agent"
var submitted: [String: String]?
let controller = NativeProfileTextEditorViewController(
title: "New chat", initialValue: "Chat", placeholder: "Chat name",
multiline: false, pageBackgroundColor: .systemGroupedBackground,
containerBackgroundColor: .secondarySystemGroupedBackground,
containerCornerRadius: 16, allowUnchangedSubmission: false,
channelDescription: nil, originalName: nil, originalDescription: nil,
canEditDetails: true, canEditCanvas: false, canvasContent: "",
canvasLoaded: false,
chatOptions: ["selectedAssistant": "agent", "savedAssistant": ["id": "agent", "name": savedName], "presentationId": "test", "assistantsLoading": empty, "fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
onCancel: {}, onSet: { submitted = $0 as? [String: String] }
)
controller.loadViewIfNeeded()
precondition(controller.numberOfSections(in: controller.tableView) == 3)
let projectCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
let project = projectCell.accessoryView as! UIButton
precondition(project.accessibilityLabel == "Project")
precondition(project.accessibilityValue == "No project")
precondition(project.titleColor(for: .normal) == .label)
precondition(project.isEnabled == !locked)
precondition(project.showsMenuAsPrimaryAction && project.menu != nil)
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
let content = cell.contentConfiguration as! UIListContentConfiguration
precondition(content.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
precondition((content.image != nil) == (locked || !empty))
let disabled = locked || empty
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
precondition((cell.selectionStyle == .none) == disabled)
let canSubmit = locked || !empty
precondition(controller.navigationItem.rightBarButtonItem?.isEnabled == canSubmit)
// Invoke the action even when disabled to check its independent gate.
_ = controller.perform(NSSelectorFromString("setTapped"))
precondition(submitted?["assistant"] == (canSubmit ? "agent" : nil))
if empty && !locked {
controller.tableView.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
controller.tableView.reloadData()
controller.tableView.layoutIfNeeded()
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 1) == nil)
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == "Loading agents…")
controller.updateChatAssistants([
"presentationId": "test",
"assistants": [["id": "agent", "name": "Agent"]],
])
controller.tableView.layoutIfNeeded()
let updated = controller.tableView.cellForRow(at: IndexPath(row: 0, section: 2))!
precondition(updated.accessibilityTraits.contains(.button))
precondition(!updated.accessibilityTraits.contains(.notEnabled))
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == nil)
}
submitted = nil
controller.updateChatAssistants([
"presentationId": "test",
"assistants": empty ? [] : [["id": "agent", "name": "Agent"]],
])
let refreshedCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
let refreshed = refreshedCell.contentConfiguration as! UIListContentConfiguration
precondition(refreshed.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
precondition(controller.navigationItem.rightBarButtonItem?.isEnabled == canSubmit)
_ = controller.perform(NSSelectorFromString("setTapped"))
precondition(submitted?["assistant"] == (canSubmit ? "agent" : nil))
}
}
avatarTests()
print("Me agent accessibility and bounded avatar loading passed")
}
}
private final class AvatarProtocol: URLProtocol {
static let lock = NSLock()
static var starts = 0
static var stops = 0
static var png = Data()
static func counts() -> (Int, Int) {
lock.lock(); defer { lock.unlock() }
return (starts, stops)
}
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
Self.lock.lock(); Self.starts += 1; Self.lock.unlock()
let path = request.url!.path
if path.hasPrefix("/hold") { return }
let headers = ["Content-Type": path == "/text" ? "text/html" : "image/png",
"Content-Length": path == "/large" ? "9000000" : "-1"]
client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: path == "/error" ? 404 : 200,
httpVersion: nil, headerFields: headers)!, cacheStoragePolicy: .notAllowed)
if path == "/chunked" {
for _ in 0..<3 { client?.urlProtocol(self, didLoad: Data(repeating: 0, count: 1024 * 1024)) }
} else {
client?.urlProtocol(self, didLoad: Self.png)
}
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {
Self.lock.lock(); Self.stops += 1; Self.lock.unlock()
}
}
@MainActor private func avatarTests() {
func pump(until predicate: () -> Bool) {
let deadline = Date().addingTimeInterval(4)
while !predicate() && Date() < deadline {
RunLoop.main.run(until: Date().addingTimeInterval(0.01))
}
precondition(predicate(), "Avatar operation timed out")
}
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [AvatarProtocol.self]
let loader = MeAvatarLoader(configuration: config)
let format = UIGraphicsImageRendererFormat()
format.scale = 1
AvatarProtocol.png = UIGraphicsImageRenderer(size: CGSize(width: 240, height: 240), format: format).pngData { _ in
UIColor.red.setFill(); UIRectFill(CGRect(x: 0, y: 0, width: 240, height: 240))
}
// No eager request on presenting even a thousand agents; only visible rows load.
let controller = MeAssistantListController(assistants: (0..<1000).map {
["id": "agent-\($0)", "name": "Agent \($0)", "avatarUrl": "https://avatar.test/hold/\($0)"]
}, selected: nil, pageColor: .white, rowColor: .white, avatars: loader, onSelect: { _, _ in })
controller.loadViewIfNeeded()
precondition(AvatarProtocol.counts().0 == 0)
for row in 0..<20 {
let path = IndexPath(row: row, section: 0)
let cell = controller.tableView(controller.tableView, cellForRowAt: path)
controller.tableView(controller.tableView, willDisplay: cell, forRowAt: path)
}
pump { AvatarProtocol.counts().0 == MeAvatarLoader.concurrency }
controller.viewDidDisappear(false)
pump { AvatarProtocol.counts().1 == MeAvatarLoader.concurrency }
precondition(AvatarProtocol.counts().0 == MeAvatarLoader.concurrency)
for path in ["large", "chunked", "error", "text", "ok"] {
var done = false
var result: UIImage?
loader.load(URL(string: "https://avatar.test/\(path)")!) { image in
result = image; done = true
}
pump { done }
precondition((result != nil) == (path == "ok"))
if let result { precondition(max(result.size.width, result.size.height) <= 120) }
}
let image = loader.image(for: URL(string: "https://avatar.test/ok"))
precondition(image != nil)
let rendered = MeAssistantListController.avatarImage(["name": "Agent"], photo: image)
precondition(rendered.size == CGSize(width: 40, height: 40))
let visibleLoader = MeAvatarLoader(configuration: config)
let visible = MeAssistantListController(assistants: [["id": "visible", "name": "Visible", "avatarUrl": "https://avatar.test/visible"]],
selected: nil, pageColor: .white, rowColor: .white, avatars: visibleLoader, onSelect: { _, _ in })
visible.loadViewIfNeeded()
visible.tableView.frame = CGRect(x: 0, y: 0, width: 320, height: 400)
visible.tableView.reloadData()
visible.tableView.layoutIfNeeded()
pump { visibleLoader.image(for: URL(string: "https://avatar.test/visible")) != nil }
let cell = visible.tableView.cellForRow(at: IndexPath(row: 0, section: 0))!
let content = cell.contentConfiguration as! UIListContentConfiguration
precondition(content.image?.pngData() == MeAssistantListController.avatarImage(["name": "Visible"], photo: image).pngData())
visible.viewDidDisappear(false)
// Source metadata is inspected before ImageIO is allowed to decode large pixels.
autoreleasepool {
let huge = UIGraphicsImageRenderer(size: CGSize(width: 4100, height: 4100), format: format).pngData { _ in }
precondition(MeAvatarLoader.thumbnail(huge) == nil)
}
loader.cancelAll()
}