// Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import AVFoundation import Flutter import XCTest @testable import camera_avfoundation private final class MockUIDevice: UIDevice { var mockOrientation: UIDeviceOrientation = .unknown override var orientation: UIDeviceOrientation { return mockOrientation } } final class CameraOrientationTests: XCTestCase { private func createCameraPlugin() -> ( cameraPlugin: CameraPlugin, mockCamera: MockCamera, mockEventAPI: MockGlobalEventApi, mockDevice: MockCaptureDevice, mockDeviceDiscoverer: MockCameraDeviceDiscoverer, captureSessionQueue: DispatchQueue ) { let mockDevice = MockCaptureDevice() let mockCamera = MockCamera() let mockEventAPI = MockGlobalEventApi() let mockDeviceDiscoverer = MockCameraDeviceDiscoverer() let captureSessionQueue = DispatchQueue(label: "io.flutter.camera.captureSessionQueue") let cameraPlugin = CameraPlugin( registry: MockFlutterTextureRegistry(), messenger: MockFlutterBinaryMessenger(), globalAPI: mockEventAPI, deviceDiscoverer: mockDeviceDiscoverer, permissionManager: MockCameraPermissionManager(), deviceFactory: { _ in mockDevice }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory(), captureSessionQueue: captureSessionQueue ) cameraPlugin.camera = mockCamera return ( cameraPlugin, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer, captureSessionQueue ) } private func sendOrientation( _ orientation: UIDeviceOrientation, to cameraPlugin: CameraPlugin, captureSessionQueue: DispatchQueue ) { cameraPlugin.orientationChanged(createMockNotification(for: orientation)) waitForQueueRoundTrip(with: captureSessionQueue) } private func createMockNotification(for deviceOrientation: UIDeviceOrientation) -> Notification { let mockDevice = MockUIDevice() mockDevice.mockOrientation = deviceOrientation return Notification(name: Notification.Name("orientation_test"), object: mockDevice) } func testOrientationNotifications() { let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() sendOrientation(.portraitUpsideDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .portraitDown) sendOrientation(.portrait, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .portraitUp) sendOrientation(.landscapeLeft, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeLeft) sendOrientation(.landscapeRight, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertEqual(mockEventAPI.lastOrientation, .landscapeRight) } func testOrientationNotificationsNotCalledForFaceUp() { let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() sendOrientation(.faceUp, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) } func testOrientationNotificationsNotCalledForFaceDown() { let (cameraPlugin, _, mockEventAPI, _, _, captureSessionQueue) = createCameraPlugin() sendOrientation(.faceDown, to: cameraPlugin, captureSessionQueue: captureSessionQueue) XCTAssertFalse(mockEventAPI.deviceOrientationChangedCalled) } func testOrientationUpdateMustBeOnCaptureSessionQueue() { let queueExpectation = expectation( description: "Orientation update must happen on the capture session queue") let (cameraPlugin, mockCamera, _, _, _, captureSessionQueue) = createCameraPlugin() let captureSessionQueueSpecific = DispatchSpecificKey() captureSessionQueue.setSpecific( key: captureSessionQueueSpecific, value: ()) mockCamera.setDeviceOrientationStub = { orientation in if DispatchQueue.getSpecific(key: captureSessionQueueSpecific) != nil { queueExpectation.fulfill() } } cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) waitForExpectations(timeout: 30, handler: nil) } func testOrientationChangedNoRetainCycle() { let (_, mockCamera, mockEventAPI, mockDevice, mockDeviceDiscoverer, _) = createCameraPlugin() let captureSessionQueue = DispatchQueue(label: "capture_session_queue") weak var weakPlugin: CameraPlugin? weak var weakDevice = mockDevice autoreleasepool { let cameraPlugin = CameraPlugin( registry: MockFlutterTextureRegistry(), messenger: MockFlutterBinaryMessenger(), globalAPI: mockEventAPI, deviceDiscoverer: mockDeviceDiscoverer, permissionManager: MockCameraPermissionManager(), deviceFactory: { _ in weakDevice! }, captureSessionFactory: { MockCaptureSession() }, captureDeviceInputFactory: MockCaptureDeviceInputFactory(), captureSessionQueue: captureSessionQueue ) weakPlugin = cameraPlugin cameraPlugin.camera = mockCamera cameraPlugin.orientationChanged(createMockNotification(for: .landscapeLeft)) } // Sanity check. let cameraDeallocatedExpectation = self.expectation( description: "Camera must have been deallocated.") captureSessionQueue.async { XCTAssertNil(weakPlugin) cameraDeallocatedExpectation.fulfill() } // Awaiting expectation is needed. The test is flaky when checking for nil right away. waitForExpectations(timeout: 1, handler: nil) var setDeviceOrientationCalled = false mockCamera.setDeviceOrientationStub = { orientation in if orientation == .landscapeLeft { setDeviceOrientationCalled = true } } weak var weakEventAPI = mockEventAPI // Must check in captureSessionQueue since orientationChanged dispatches to this queue. let expectation = self.expectation(description: "Dispatched to capture session queue") captureSessionQueue.async { XCTAssertFalse(setDeviceOrientationCalled) XCTAssertFalse(weakEventAPI?.deviceOrientationChangedCalled ?? false) expectation.fulfill() } waitForExpectations(timeout: 30, handler: nil) } }