mirror of
https://github.com/block/buzz.git
synced 2026-11-13 12:51:34 +00:00
Allow locked retries to return their saved agent to Flutter for authoritative ownership revalidation. Keep inventory membership required for new chats and cover both native button and submit gates across inventory refreshes. Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez <klopez4212@gmail.com>
167 lines
8.4 KiB
Swift
167 lines
8.4 KiB
Swift
import Flutter
|
|
import UIKit
|
|
|
|
@main
|
|
struct MeAgentAccessibilityTests {
|
|
@MainActor static func main() {
|
|
for locked in [false, true] {
|
|
for empty in [false, true] {
|
|
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", "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 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"]],
|
|
])
|
|
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()
|
|
}
|