Flutter iOS Embedder
FlutterViewTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #import <OCMock/OCMock.h>
6 #import <XCTest/XCTest.h>
7 
14 
16 
18 - (BOOL)isWideGamutSupported;
19 @end
20 
21 @interface FakeDelegate : NSObject <FlutterViewEngineDelegate>
22 @property(nonatomic) BOOL callbackCalled;
23 @end
24 
25 @implementation FakeDelegate
26 
27 @synthesize platformViewsController = _platformViewsController;
28 
29 - (instancetype)init {
30  _callbackCalled = NO;
31  return self;
32 }
33 
34 - (flutter::Rasterizer::Screenshot)takeScreenshot:(flutter::Rasterizer::ScreenshotType)type
35  asBase64Encoded:(BOOL)base64Encode {
36  return {};
37 }
38 
40  _callbackCalled = YES;
41 }
42 
43 @end
44 
45 @interface FlutterViewTest : XCTestCase
46 @end
47 
48 @implementation FlutterViewTest
49 
50 - (void)testFlutterViewEnableSemanticsWhenIsAccessibilityElementIsCalled {
51  FakeDelegate* delegate = [[FakeDelegate alloc] init];
52  FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate opaque:NO enableWideGamut:NO];
53  delegate.callbackCalled = NO;
54  XCTAssertFalse(view.isAccessibilityElement);
55  XCTAssertTrue(delegate.callbackCalled);
56 }
57 
58 - (void)testFlutterViewBackgroundColorIsNil {
59  FakeDelegate* delegate = [[FakeDelegate alloc] init];
60  FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate opaque:NO enableWideGamut:NO];
61  XCTAssertNil(view.backgroundColor);
62 }
63 
64 - (void)testLayerScalesMatchScreenAfterLayoutSubviews {
65  FakeDelegate* delegate = [[FakeDelegate alloc] init];
66  FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate opaque:NO enableWideGamut:NO];
67  view.layer.contentsScale = CGFloat(-99.0);
68  view.layer.rasterizationScale = CGFloat(-99.0);
69  UIScreen* screen = [view screen];
70  XCTAssertNotEqual(view.layer.contentsScale, screen.scale);
71  XCTAssertNotEqual(view.layer.rasterizationScale, screen.scale);
72  [view layoutSubviews];
73  XCTAssertEqual(view.layer.contentsScale, screen.scale);
74  XCTAssertEqual(view.layer.rasterizationScale, screen.scale);
75 }
76 
77 - (void)testViewWillMoveToWindow {
78  NSDictionary* mocks = [self createWindowMocks];
79  FlutterView* view = (FlutterView*)mocks[@"view"];
80  id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"];
81  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate =
82  (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"];
83  id mockEngine = mocks[@"mockEngine"];
84  id mockWindow = mocks[@"mockWindow"];
85 
86  [view willMoveToWindow:mockWindow];
87  OCMVerify(times(1), [mockLifecycleDelegate addFlutterManagedEngine:mockEngine]);
88  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 1.0);
89 }
90 
91 - (void)testViewWillMoveToSameWindow {
92  NSDictionary* mocks = [self createWindowMocks];
93  FlutterView* view = (FlutterView*)mocks[@"view"];
94  id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"];
95  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate =
96  (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"];
97  id mockEngine = mocks[@"mockEngine"];
98  id mockWindow = mocks[@"mockWindow"];
99 
100  [view willMoveToWindow:mockWindow];
101  [view willMoveToWindow:mockWindow];
102 
103  OCMVerify(times(2), [mockLifecycleDelegate addFlutterManagedEngine:mockEngine]);
104  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 1.0);
105 }
106 
107 - (void)testMultipleViewsWillMoveToSameWindow {
108  NSDictionary* mocks = [self createWindowMocks];
109  FlutterView* view1 = (FlutterView*)mocks[@"view"];
110  id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"];
111  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate =
112  (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"];
113  id mockEngine1 = mocks[@"mockEngine"];
114  id mockWindow1 = mocks[@"mockWindow"];
115 
116  id mockEngine2 = OCMClassMock([FlutterEngine class]);
117  FlutterView* view2 = [[FlutterView alloc] initWithDelegate:mockEngine2
118  opaque:NO
119  enableWideGamut:NO];
120 
121  [view1 willMoveToWindow:mockWindow1];
122  [view2 willMoveToWindow:mockWindow1];
123  [view1 willMoveToWindow:mockWindow1];
124  OCMVerify(times(2), [mockLifecycleDelegate addFlutterManagedEngine:mockEngine1]);
125  OCMVerify(times(1), [mockLifecycleDelegate addFlutterManagedEngine:mockEngine2]);
126  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 2.0);
127 }
128 
129 - (void)testMultipleViewsWillMoveToDifferentWindow {
130  NSDictionary* mocks = [self createWindowMocks];
131  FlutterView* view1 = (FlutterView*)mocks[@"view"];
132  id mockLifecycleDelegate1 = mocks[@"mockLifecycleDelegate"];
133  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate1 =
134  (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"];
135  id mockEngine1 = mocks[@"mockEngine"];
136  id mockWindow1 = mocks[@"mockWindow"];
137 
138  NSDictionary* mocks2 = [self createWindowMocks];
139  FlutterView* view2 = (FlutterView*)mocks2[@"view"];
140  id mockLifecycleDelegate2 = mocks2[@"mockLifecycleDelegate"];
141  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate2 =
142  (FlutterPluginSceneLifeCycleDelegate*)mocks2[@"lifecycleDelegate"];
143  id mockEngine2 = mocks2[@"mockEngine"];
144  id mockWindow2 = mocks2[@"mockWindow"];
145 
146  [view1 willMoveToWindow:mockWindow1];
147  [view2 willMoveToWindow:mockWindow2];
148  [view1 willMoveToWindow:mockWindow1];
149  OCMVerify(times(2), [mockLifecycleDelegate1 addFlutterManagedEngine:mockEngine1]);
150  OCMVerify(times(1), [mockLifecycleDelegate2 addFlutterManagedEngine:mockEngine2]);
151  XCTAssertEqual(lifecycleDelegate1.flutterManagedEngines.count, 1.0);
152  XCTAssertEqual(lifecycleDelegate2.flutterManagedEngines.count, 1.0);
153 }
154 
155 - (void)testViewRemovedFromWindowAndAddedToNewScene {
156  NSDictionary* mocks = [self createWindowMocks];
157  FlutterView* view = (FlutterView*)mocks[@"view"];
158  id mockLifecycleDelegate = mocks[@"mockLifecycleDelegate"];
159  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate =
160  (FlutterPluginSceneLifeCycleDelegate*)mocks[@"lifecycleDelegate"];
161  id mockEngine = mocks[@"mockEngine"];
162  id mockWindow = mocks[@"mockWindow"];
163 
164  NSDictionary* mocks2 = [self createWindowMocks];
165  id mockWindow2 = mocks2[@"mockWindow"];
166  id mockLifecycleDelegate2 = mocks2[@"mockLifecycleDelegate"];
167  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate2 =
168  (FlutterPluginSceneLifeCycleDelegate*)mocks2[@"lifecycleDelegate"];
169 
170  id mockView = OCMPartialMock(view);
171 
172  [mockView willMoveToWindow:mockWindow];
173  OCMVerify(times(1), [mockLifecycleDelegate addFlutterManagedEngine:mockEngine]);
174  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 1.0);
175 
176  OCMStub([mockView window]).andReturn(mockWindow);
177  [mockView willMoveToWindow:nil];
178  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 1.0);
179 
180  OCMStub([mockView window]).andReturn(nil);
181  [mockView willMoveToWindow:mockWindow2];
182  OCMVerify(times(1), [mockLifecycleDelegate removeFlutterManagedEngine:mockEngine]);
183  XCTAssertEqual(lifecycleDelegate.flutterManagedEngines.count, 0.0);
184  OCMVerify(times(1), [mockLifecycleDelegate2 addFlutterManagedEngine:mockEngine]);
185  XCTAssertEqual(lifecycleDelegate2.flutterManagedEngines.count, 1.0);
186 }
187 
188 - (NSDictionary*)createWindowMocks {
189  return [self createWindowMocksWithWideGamut:NO];
190 }
191 
192 - (NSDictionary*)createWindowMocksWithWideGamut:(BOOL)enableWideGamut {
193  id mockEngine = OCMClassMock([FlutterEngine class]);
194  FlutterView* view = [[FlutterView alloc] initWithDelegate:mockEngine
195  opaque:NO
196  enableWideGamut:enableWideGamut];
197  id mockWindow = OCMClassMock([UIWindow class]);
198  id mockWindowScene = OCMClassMock([UIWindowScene class]);
199 
200  FlutterSceneDelegate* sceneDelegate = [[FlutterSceneDelegate alloc] init];
201  id mockSceneDelegate = OCMPartialMock(sceneDelegate);
202 
203  FlutterPluginSceneLifeCycleDelegate* lifecycleDelegate =
205  id mockLifecycleDelegate = OCMPartialMock(lifecycleDelegate);
206 
207  OCMStub([mockWindow windowScene]).andReturn(mockWindowScene);
208  OCMStub([mockWindowScene delegate]).andReturn(mockSceneDelegate);
209  OCMStub([mockSceneDelegate sceneLifeCycleDelegate]).andReturn(mockLifecycleDelegate);
210 
211  return @{
212  @"view" : view,
213  @"mockLifecycleDelegate" : mockLifecycleDelegate,
214  @"lifecycleDelegate" : lifecycleDelegate,
215  @"mockEngine" : mockEngine,
216  @"mockWindow" : mockWindow,
217  };
218 }
219 
220 #pragma mark - Wide Gamut Tests
221 
222 // Helper: add FlutterView to a real UIWindow so that layoutSubviews can access screen.
223 - (FlutterView*)createViewInWindowWithWideGamut:(BOOL)enableWideGamut {
224  FakeDelegate* delegate = [[FakeDelegate alloc] init];
225  FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate
226  opaque:NO
227  enableWideGamut:enableWideGamut];
228  // Add to a real window so layoutSubviews has access to screen.
229  UIWindow* window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
230  [window addSubview:view];
231  view.frame = window.bounds;
232  [view layoutSubviews];
233  return view;
234 }
235 
236 - (void)testWideGamutViewSetsBGRA10XRPixelFormat {
237  FlutterView* view = [self createViewInWindowWithWideGamut:YES];
238  // On a wide gamut capable device, the pixel format should be BGRA10_XR.
239  // On non-wide-gamut devices, it falls back to BGRA8Unorm.
240  if ([view isWideGamutSupported]) {
241  XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA10_XR);
242  } else {
243  XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA8Unorm);
244  }
245 }
246 
247 - (void)testStandardGamutViewKeepsBGRA8Unorm {
248  FlutterView* view = [self createViewInWindowWithWideGamut:NO];
249  XCTAssertEqual(view.pixelFormat, MTLPixelFormatBGRA8Unorm);
250 }
251 
252 - (void)testWideGamutViewSetsExtendedSRGBColorSpace {
253  FlutterView* view = [self createViewInWindowWithWideGamut:YES];
254  if ([view isWideGamutSupported]) {
255  CAMetalLayer* layer = (CAMetalLayer*)view.layer;
256  CGColorSpaceRef colorSpace = layer.colorspace;
257  XCTAssertNotNil((__bridge id)colorSpace);
258  CGColorSpaceRef extendedSRGB = CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB);
259  XCTAssertTrue(CFEqual(colorSpace, extendedSRGB));
260  CGColorSpaceRelease(extendedSRGB);
261  }
262 }
263 
264 - (void)testStandardGamutViewDoesNotSetExtendedColorSpace {
265  FlutterView* view = [self createViewInWindowWithWideGamut:NO];
266  CAMetalLayer* layer = (CAMetalLayer*)view.layer;
267  // Default CAMetalLayer colorspace is nil (device default sRGB).
268  XCTAssertNil((__bridge id)layer.colorspace);
269 }
270 
271 #pragma mark - FlutterOverlayView Wide Gamut Tests
272 
273 - (void)testOverlayViewWideGamutSetsBGRA10XR {
274  FlutterOverlayView* overlay =
275  [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA10_XR];
276  CAMetalLayer* layer = (CAMetalLayer*)overlay.layer;
277  XCTAssertEqual(layer.pixelFormat, MTLPixelFormatBGRA10_XR);
278 }
279 
280 - (void)testOverlayViewWideGamutSetsExtendedSRGBColorSpace {
281  FlutterOverlayView* overlay =
282  [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA10_XR];
283  CAMetalLayer* layer = (CAMetalLayer*)overlay.layer;
284  CGColorSpaceRef colorSpace = layer.colorspace;
285  XCTAssertNotNil((__bridge id)colorSpace);
286  CGColorSpaceRef extendedSRGB = CGColorSpaceCreateWithName(kCGColorSpaceExtendedSRGB);
287  XCTAssertTrue(CFEqual(colorSpace, extendedSRGB));
288  CGColorSpaceRelease(extendedSRGB);
289 }
290 
291 - (void)testOverlayViewStandardGamutKeepsBGRA8Unorm {
292  FlutterOverlayView* overlay =
293  [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA8Unorm];
294  CAMetalLayer* layer = (CAMetalLayer*)overlay.layer;
295  XCTAssertEqual(layer.pixelFormat, MTLPixelFormatBGRA8Unorm);
296 }
297 
298 - (void)testOverlayViewStandardGamutDoesNotSetExtendedColorSpace {
299  FlutterOverlayView* overlay =
300  [[FlutterOverlayView alloc] initWithContentsScale:2.0 pixelFormat:MTLPixelFormatBGRA8Unorm];
301  CAMetalLayer* layer = (CAMetalLayer*)overlay.layer;
302  XCTAssertNil((__bridge id)layer.colorspace);
303 }
304 
305 - (void)testOverlayViewContentsScaleIsSet {
306  FlutterOverlayView* overlay =
307  [[FlutterOverlayView alloc] initWithContentsScale:3.0 pixelFormat:MTLPixelFormatBGRA10_XR];
308  XCTAssertEqual(overlay.layer.contentsScale, 3.0);
309  XCTAssertEqual(overlay.layer.rasterizationScale, 3.0);
310 }
311 
312 @end
UIScreen * screen()
Definition: FlutterView.mm:42
MTLPixelFormat pixelFormat()
Definition: FlutterView.mm:111
FlutterPlatformViewsController * platformViewsController
Definition: FlutterView.h:16