Files
buzz/mobile/ios/NativeControlTests/MeAgentAccessibilityTests.swift
klopez4212 b14884980e Speed up mobile Solo chat creation and agent discovery (#8547)
## Summary

Solo compose now opens before agent discovery finishes, and chat
creation avoids full channel-list reloads while overlapping independent
reads. Durable retries, ownership checks, and relay confirmation remain
intact.

The agent picker uses the owner's saved inventory, excludes known
archived identities, verifies profiles off the UI thread, and reuses
avatar artwork.

### Related issue

No matching issue found. Related: #8546 adds project selection to the
same compose surface.

### Testing

- Full mobile suite: 3,755 passed, 19 skipped; four additional artifact
tests passed.
- Analysis, formatting, file-size checks, and iOS Release build passed.
- Regression tests fail when the old blocking paths are restored.
- Installed on a physical iPhone with existing profile/community
settings preserved. Owner tested the earlier creation/picker
improvements; final compose timing awaits device feedback. No timing
recording captured.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-27 15:09:22 +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()
}