Files
buzz/mobile/ios/NativeControlTests/MeAgentAccessibilityTests.swift
klopez4212 dc25198d53 Add project selection to new Solo chats on mobile (#8546)
New Solo chats can optionally choose a project in the Flutter form and
native iOS sheet. Project appears above the agent picker with matching
neutral text, and the choice survives retries and restarts.

Validation: full Flutter suite passed; analysis and iOS builds passed.
Installed in Pairing Preview on iPhone Air and visually approved,
preserving saved preferences. Successful live chat creation remains
unverified because no agents were available.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-22 15:33:49 -07:00

137 lines
6.5 KiB
Swift

import Flutter
import UIKit
@main
struct MeAgentAccessibilityTests {
@MainActor static func main() {
for locked in [false, true] {
for empty in [false, true] {
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: ["fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
onCancel: {}, onSet: { _ in }
)
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 disabled = locked || empty
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
precondition((cell.selectionStyle == .none) == disabled)
}
}
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()
}