mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
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>
137 lines
6.5 KiB
Swift
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()
|
|
}
|