~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
122 lines · 4.6 KB Raw History
@testable import Grain
import SwiftUI
import XCTest

/// The carousel above the photo strip in the create sheet. It is the only place
/// a chosen photo is shown at full width, and it decodes a screen-sized preview
/// per photo to do it.
@MainActor
final class PhotoCarouselRenderTests: GrainTestCase {
    private var account: TestAccount!

    /// Async overrides: the synchronous `setUp`/`tearDown` are nonisolated, so
    /// touching main-actor state from them warns.
    override func setUp() async throws {
        try await super.setUp()
        account = TestAccount()
        MockURLProtocol.respondByPath(Fixtures.routes)
    }

    override func tearDown() async throws {
        MockURLProtocol.handler = nil
        account.restore()
        try await super.tearDown()
    }

    private func makeItems(_ count: Int, mixedRatios: Bool = false, withExif: Bool = false, alt: Bool = false) -> [PhotoItem] {
        let exif = ExifSummary(
            camera: "Fujifilm X100V",
            lens: "23mm f/2",
            exposure: nil,
            shutterSpeed: "1/500s",
            iso: "ISO 400",
            focalLength: "35mm",
            aperture: "f/2"
        )
        return (0 ..< count).map { index in
            let size = mixedRatios && !index.isMultiple(of: 2)
                ? CGSize(width: 60, height: 90)
                : CGSize(width: 120, height: 80)
            let image = UIGraphicsImageRenderer(size: size).image { context in
                context.cgContext.setFillColor(red: 0.2, green: 0.5, blue: 0.8, alpha: 1)
                context.cgContext.fill(CGRect(origin: .zero, size: size))
            }
            return PhotoItem(
                thumbnail: image,
                carouselPreview: image,
                source: .camera(image, metadata: nil),
                alt: alt ? "Alt text for photo \(index)" : "",
                exifSummary: withExif ? exif : nil
            )
        }
    }

    private func renderCarousel(
        items: [PhotoItem],
        selected: UUID?,
        sendExif: Bool = true,
        settle: TimeInterval = 0.25
    ) {
        var bound = selected
        ViewRender.render(
            PhotoCarouselView(
                items: items,
                selectedPhotoID: Binding(get: { bound }, set: { bound = $0 }),
                sendExif: sendExif
            )
            .withTestEnvironment(TestEnvironment()),
            settle: settle
        )
    }

    /// The carousel is what the create sheet shows above the strip, and it
    /// decodes a screen-width preview per photo on the way.
    func testRendersTheCarouselWithSeveralPhotos() {
        let items = makeItems(4)
        renderCarousel(items: items, selected: items.first?.id)
    }

    /// Mixed aspect ratios make the carousel size to a common ratio rather than
    /// to each page, which is a separate measurement path.
    func testRendersTheCarouselWithMixedAspectRatios() {
        let items = makeItems(4, mixedRatios: true)
        renderCarousel(items: items, selected: items.first?.id)
    }

    /// Exif turns on a whole row under the photo; alt text turns on a badge.
    func testRendersTheCarouselWithExifAndAltText() {
        let items = makeItems(3, withExif: true, alt: true)
        renderCarousel(items: items, selected: items.first?.id)
        renderCarousel(items: items, selected: items.first?.id, sendExif: false)
    }

    /// Paging to a later photo is what the strip does on selection.
    func testRendersTheCarouselOnALaterPhoto() {
        let items = makeItems(5)
        renderCarousel(items: items, selected: items[3].id)
        renderCarousel(items: items, selected: items.last?.id)
    }

    /// A selection that no longer exists — the photo it named was deleted — has
    /// to clamp rather than index out of bounds.
    func testTheCarouselSurvivesASelectionThatIsGone() {
        renderCarousel(items: makeItems(3), selected: UUID())
    }

    func testTheCarouselWithASinglePhotoHasNoPager() {
        let items = makeItems(1)
        renderCarousel(items: items, selected: items.first?.id)
    }

    /// Built before the picker returns, so it has to lay out nothing.
    func testTheCarouselWithNoPhotosDrawsNothing() {
        let empty = UIHostingController(
            rootView: PhotoCarouselView(items: [], selectedPhotoID: .constant(nil), sendExif: true)
        ).sizeThatFits(in: CGSize(width: 402, height: 874))
        let filled = UIHostingController(
            rootView: PhotoCarouselView(items: makeItems(2), selectedPhotoID: .constant(nil), sendExif: true)
        ).sizeThatFits(in: CGSize(width: 402, height: 874))

        XCTAssertLessThan(empty.height, filled.height)
    }
}