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

grain.social/grain-ios

1 collaborator
0
1
main
87 lines · 2.7 KB Raw History
import AVFoundation
import Foundation
@testable import Grain
import Testing

/// The story camera's state machine, as far as it can be driven without a
/// camera. The simulator has none, so the ready state is device-only; what
/// these pin is that every path short of it ends in a state the capture stage
/// knows how to draw, and that nothing tries to shoot from those states.
@MainActor
struct StoryCameraTests {
    @Test func deniedAccessLeavesTheCameraUnauthorized() async {
        let camera = StoryCamera(requestAccess: { false })

        await camera.start()

        #expect(camera.status == .unauthorized)
        #expect(await camera.capture() == nil, "there is nothing to shoot with")
    }

    /// Granted access with no camera behind it — the simulator — must land on
    /// the unavailable placeholder rather than hang in `starting`.
    @Test func grantedAccessWithoutACameraIsUnavailable() async {
        let camera = StoryCamera(requestAccess: { true })

        await camera.start()

        #expect(camera.status == .unavailable)
        #expect(await camera.capture() == nil)
    }

    @Test func theCameraStartsOutStartingOnTheBackLensWithFlashOff() {
        let camera = StoryCamera(requestAccess: { true })

        #expect(camera.status == .starting)
        #expect(camera.position == .back)
        #expect(camera.flashMode == .off)
        #expect(!camera.hasFlash, "flash is only offered once a lens reports having one")
        #expect(!camera.isCapturing)
    }

    @Test func flashTogglesBetweenOffAndOn() {
        let camera = StoryCamera(requestAccess: { true })

        camera.toggleFlash()
        #expect(camera.flashMode == .on)
        camera.toggleFlash()
        #expect(camera.flashMode == .off)
    }

    /// A flip that can't find the other lens keeps the current one, so the
    /// controls don't claim a lens that isn't feeding the preview.
    @Test func flippingWithoutACameraKeepsThePosition() async {
        let camera = StoryCamera(requestAccess: { true })
        await camera.start()

        await camera.flip()

        #expect(camera.position == .back)
    }

    @Test func nothingIsAskedOfTheSessionUntilStart() async {
        let asked = Flag()
        let camera = StoryCamera(requestAccess: {
            asked.raise()
            return false
        })

        #expect(!asked.isRaised, "the permission prompt waits for the composer to appear")
        await camera.start()
        #expect(asked.isRaised)
    }
}

/// A flag a `@Sendable` closure can raise.
private final class Flag: @unchecked Sendable {
    private let lock = NSLock()
    private var raised = false

    var isRaised: Bool {
        lock.withLock { raised }
    }

    func raise() {
        lock.withLock { raised = true }
    }
}