oficina / meet Public

Tests: iPhone glyph centroids on the simulator; narrow tile label truncates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0c4831cb3f0aa3a0b1ac60bf9de52147fa799377 parent f77d525
2 files changed, +133 −0
  • native/apple/Tests/CallsKitTests/CallGlyphTests.swift +28 −0
  • native/ios/UITests/CallsPhoneUITests.swift +105 −0
diff --git a/native/apple/Tests/CallsKitTests/CallGlyphTests.swift b/native/apple/Tests/CallsKitTests/CallGlyphTests.swift
index 95190bb..c7195a1 100644
--- a/native/apple/Tests/CallsKitTests/CallGlyphTests.swift
+++ b/native/apple/Tests/CallsKitTests/CallGlyphTests.swift
@@ -206,3 +206,31 @@ struct Ink {
return Ink(size: size, centroidOffset: CGFloat(centroid - Double(size.height) / 2))
}
}
+
+/// A filmstrip tile's name label (0.4.1): on a narrow tile it stays
+/// inside the tile, the name truncating and "Muted" giving way to the
+/// slashed microphone alone, and on a wide one it says it all.
+@MainActor final class TileNameLabelTests: XCTestCase {
+ private func width(_ label: TileNameLabel, in proposed: CGFloat) -> CGFloat {
+ let host = NSHostingController(rootView: label)
+ return host.sizeThatFits(in: CGSize(width: proposed, height: 100)).width
+ }
+
+ func testANarrowTileTruncatesTheNameAndKeepsTheMark() {
+ let label = TileNameLabel(label: "Ana García Fernández", muted: true)
+ let wide = width(label, in: 1000)
+ let narrow = width(label, in: 100)
+ XCTAssertLessThanOrEqual(narrow, 100, "the label stays inside a narrow tile")
+ XCTAssertLessThan(narrow, wide)
+ // What the mark takes with the word, and without it.
+ let withWord = wide - width(TileNameLabel(label: "Ana García Fernández", muted: false), in: 1000)
+ XCTAssertGreaterThan(withWord, 40, "a wide tile shows the word Muted as well as the symbol")
+ let unmutedNarrow = width(TileNameLabel(label: "Ana García Fernández", muted: false), in: 100)
+ XCTAssertLessThanOrEqual(unmutedNarrow, 100)
+ }
+
+ func testAShortNameIsNotTruncated() {
+ let label = TileNameLabel(label: "Li", muted: true)
+ XCTAssertEqual(width(label, in: 1000), width(label, in: 160), accuracy: 0.5)
+ }
+}
diff --git a/native/ios/UITests/CallsPhoneUITests.swift b/native/ios/UITests/CallsPhoneUITests.swift
index e60eca2..f74b276 100644
--- a/native/ios/UITests/CallsPhoneUITests.swift
+++ b/native/ios/UITests/CallsPhoneUITests.swift
@@ -1,3 +1,4 @@
+import UIKit
import XCTest
final class CallsPhoneUITests: XCTestCase {
@@ -396,4 +397,108 @@ final class CallsPhoneUITests: XCTestCase {
XCTAssertTrue(app.buttons["moreControls"].isHittable)
XCTAssertLessThan(app.buttons["moreControls"].frame.midX, app.buttons["leave"].frame.midX)
}
+
+ // MARK: Glyphs (0.4.1)
+
+ /// Paul, on the 0.4.0 bar: "Are these vertically aligned?" Every
+ /// symbol a call control draws goes through CallGlyph and its optical
+ /// table; the Mac's tests measure it drawn by AppKit, and this measures
+ /// it as the iPhone draws it: the round bar buttons, the microphone
+ /// beside the menu, and every button in the controls sheet, each
+ /// symbol's ink within half a point of its button's centre line.
+ @MainActor func testControlGlyphsAreOnTheirCentreLines() throws {
+ XCUIDevice.shared.orientation = .portrait
+ var report: [String] = []
+ var failures: [String] = []
+ func measure(_ app: XCUIApplication, _ ids: [String], _ label: String) {
+ let shot = XCUIScreen.main.screenshot()
+ for id in ids {
+ let button = app.buttons[id].firstMatch
+ guard button.exists else {
+ failures.append("\(label): \(id) is not there")
+ continue
+ }
+ guard let offset = GlyphInk.offset(in: shot.image, frame: button.frame) else {
+ failures.append("\(label): \(id) drew no symbol")
+ continue
+ }
+ report.append(String(format: "%@ %@: %+.2f pt", label, id, offset))
+ if abs(offset) > 0.5 { failures.append(String(format: "%@ %@: %+.2f pt", label, id, offset)) }
+ }
+ }
+ let bar = launchCall(
+ "focus",
+ ["-CallsFixtureControls", "shown", "-CallsFixtureBackdrop", "black", "-CallsMicBesideMenu", "YES"],
+ dark: true)
+ XCTAssertTrue(bar.buttons["moreControls"].waitForExistence(timeout: 15))
+ Thread.sleep(forTimeInterval: 1)
+ measure(bar, ["moreControls", "mute", "leave"], "bar")
+ bar.terminate()
+ for dark in [false, true] {
+ let sheet = launchCall("focus", ["-CallsFixtureControls", "shown", "-CallsFixtureMore", "YES"], dark: dark)
+ XCTAssertTrue(sheet.buttons["devices"].waitForExistence(timeout: 15))
+ Thread.sleep(forTimeInterval: 1.5)
+ measure(
+ sheet, ["mute", "camera", "flipCamera", "devices", "micModeQuick", "videoLayout", "Share meeting link", "openChat"],
+ dark ? "sheet dark" : "sheet light")
+ sheet.terminate()
+ }
+ let attachment = XCTAttachment(string: report.joined(separator: "\n"))
+ attachment.name = "glyph centroids"
+ attachment.lifetime = .keepAlways
+ add(attachment)
+ try FileManager.default.createDirectory(at: snapshotDirectory, withIntermediateDirectories: true)
+ try report.joined(separator: "\n").write(
+ to: snapshotDirectory.appendingPathComponent("iphone-glyph-centroids.txt"), atomically: true,
+ encoding: .utf8)
+ XCTAssert(failures.isEmpty, failures.joined(separator: "\n") + "\n" + report.joined(separator: "\n"))
+ }
+}
+
+/// Where a button's symbol's ink is: the pixels of its frame weighted by
+/// how far their colour is from the button's own background, sampled
+/// just inside its left edge at mid-height (a round button's glass, a
+/// tinted square's fill, a red capsule), as the tests on the Mac do.
+enum GlyphInk {
+ static func offset(in image: UIImage, frame: CGRect) -> CGFloat? {
+ guard let cg = image.cgImage else { return nil }
+ let scale = CGFloat(cg.width) / image.size.width
+ let rect = CGRect(
+ x: (frame.minX * scale).rounded(), y: (frame.minY * scale).rounded(),
+ width: (frame.width * scale).rounded(), height: (frame.height * scale).rounded())
+ guard let crop = cg.cropping(to: rect) else { return nil }
+ let w = crop.width, h = crop.height
+ var px = [UInt8](repeating: 0, count: w * h * 4)
+ guard
+ let ctx = CGContext(
+ data: &px, width: w, height: h, bitsPerComponent: 8, bytesPerRow: w * 4,
+ space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
+ else { return nil }
+ ctx.draw(crop, in: CGRect(x: 0, y: 0, width: w, height: h))
+ // The background: the median of a short column just inside the
+ // left edge, clear of the symbol and of a rounded corner.
+ let x = max(2, Int(4 * scale))
+ var samples: [(Int, Int, Int)] = []
+ for y in (h / 2 - Int(3 * scale))...(h / 2 + Int(3 * scale)) {
+ let i = (y * w + x) * 4
+ samples.append((Int(px[i]), Int(px[i + 1]), Int(px[i + 2])))
+ }
+ samples.sort { $0.0 + $0.1 + $0.2 < $1.0 + $1.1 + $1.2 }
+ let bg = samples[samples.count / 2]
+ var total = 0.0, moment = 0.0
+ // Only the middle of the button, where the symbol is: a square's
+ // rounded corners and a circle's edge stay out of it.
+ let inset = Int(8 * scale)
+ for y in inset..<(h - inset) {
+ for x in inset..<(w - inset) {
+ let i = (y * w + x) * 4
+ let d = abs(Int(px[i]) - bg.0) + abs(Int(px[i + 1]) - bg.1) + abs(Int(px[i + 2]) - bg.2)
+ let weight = Double(max(0, d - 60))
+ total += weight
+ moment += weight * (Double(y) + 0.5)
+ }
+ }
+ guard total > 0 else { return nil }
+ return CGFloat(moment / total) / scale - frame.height / 2 + (rect.minY / scale - frame.minY)
+ }
}