| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ui/views/cocoa/bridged_native_widget.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #import "base/mac/mac_util.h" |
| #import "base/mac/sdk_forward_declarations.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "ui/base/hit_test.h" |
| #import "ui/base/test/nswindow_fullscreen_notification_waiter.h" |
| #include "ui/base/test/ui_controls.h" |
| #import "ui/base/test/windowed_nsnotification_observer.h" |
| #import "ui/events/test/cocoa_test_event_utils.h" |
| #include "ui/views/test/views_interactive_ui_test_base.h" |
| #include "ui/views/test/widget_test.h" |
| #include "ui/views/widget/native_widget_mac.h" |
| #include "ui/views/window/native_frame_view.h" |
| |
| namespace views { |
| namespace test { |
| namespace { |
| |
| // Provide a resizable Widget by default. Starting in 10.11, OSX doesn't |
| // correctly restore the window size when coming out of fullscreen if the window |
| // is not user-sizable. |
| class ResizableDelegateView : public WidgetDelegateView { |
| public: |
| ResizableDelegateView() {} |
| |
| // WidgetDelgate: |
| bool CanResize() const override { return true; } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ResizableDelegateView); |
| }; |
| |
| } // namespace |
| |
| class BridgedNativeWidgetUITest : public test::WidgetTest { |
| public: |
| BridgedNativeWidgetUITest() = default; |
| |
| // testing::Test: |
| void SetUp() override { |
| ViewsInteractiveUITestBase::InteractiveSetUp(); |
| WidgetTest::SetUp(); |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::TYPE_WINDOW); |
| init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| init_params.bounds = gfx::Rect(100, 100, 300, 200); |
| init_params.delegate = new ResizableDelegateView; |
| widget_.reset(new Widget); |
| widget_->Init(init_params); |
| } |
| |
| void TearDown() override { |
| // Ensures any compositor is removed before ViewsTestBase tears down the |
| // ContextFactory. |
| widget_.reset(); |
| WidgetTest::TearDown(); |
| } |
| |
| NSWindow* test_window() { |
| return widget_->GetNativeWindow(); |
| } |
| |
| protected: |
| std::unique_ptr<Widget> widget_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetUITest); |
| }; |
| |
| // Tests for correct fullscreen tracking, regardless of whether it is initiated |
| // by the Widget code or elsewhere (e.g. by the user). |
| TEST_F(BridgedNativeWidgetUITest, FullscreenSynchronousState) { |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| |
| // Allow user-initiated fullscreen changes on the Window. |
| [test_window() |
| setCollectionBehavior:[test_window() collectionBehavior] | |
| NSWindowCollectionBehaviorFullScreenPrimary]; |
| |
| base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter( |
| [[NSWindowFullscreenNotificationWaiter alloc] |
| initWithWindow:test_window()]); |
| const gfx::Rect restored_bounds = widget_->GetRestoredBounds(); |
| |
| // First show the widget. A user shouldn't be able to initiate fullscreen |
| // unless the window is visible in the first place. |
| widget_->Show(); |
| |
| // Simulate a user-initiated fullscreen. Note trying to to this again before |
| // spinning a runloop will cause Cocoa to emit text to stdio and ignore it. |
| [test_window() toggleFullScreen:nil]; |
| EXPECT_TRUE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| // Note there's now an animation running. While that's happening, toggling the |
| // state should work as expected, but do "nothing". |
| widget_->SetFullscreen(false); |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| widget_->SetFullscreen(false); // Same request - should no-op. |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| widget_->SetFullscreen(true); |
| EXPECT_TRUE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| // Always finish out of fullscreen. Otherwise there are 4 NSWindow objects |
| // that Cocoa creates which don't close themselves and will be seen by the Mac |
| // test harness on teardown. Note that the test harness will be waiting until |
| // all animations complete, since these temporary animation windows will not |
| // be removed from the window list until they do. |
| widget_->SetFullscreen(false); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| // Now we must wait for the notifications. Since, if the widget is torn down, |
| // the NSWindowDelegate is removed, and the pending request to take out of |
| // fullscreen is lost. Since a message loop has not yet spun up in this test |
| // we can reliably say there will be one enter and one exit, despite all the |
| // toggling above. |
| [waiter waitForEnterCount:1 exitCount:1]; |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| } |
| |
| // Test fullscreen without overlapping calls and without changing collection |
| // behavior on the test window. |
| TEST_F(BridgedNativeWidgetUITest, FullscreenEnterAndExit) { |
| base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter( |
| [[NSWindowFullscreenNotificationWaiter alloc] |
| initWithWindow:test_window()]); |
| |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| const gfx::Rect restored_bounds = widget_->GetRestoredBounds(); |
| EXPECT_FALSE(restored_bounds.IsEmpty()); |
| |
| // Ensure this works without having to change collection behavior as for the |
| // test above. Also check that making a hidden widget fullscreen shows it. |
| EXPECT_FALSE(widget_->IsVisible()); |
| widget_->SetFullscreen(true); |
| EXPECT_TRUE(widget_->IsVisible()); |
| |
| EXPECT_TRUE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| // Should be zero until the runloop spins. |
| EXPECT_EQ(0, [waiter enterCount]); |
| [waiter waitForEnterCount:1 exitCount:0]; |
| |
| // Verify it hasn't exceeded. |
| EXPECT_EQ(1, [waiter enterCount]); |
| EXPECT_EQ(0, [waiter exitCount]); |
| EXPECT_TRUE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| widget_->SetFullscreen(false); |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| |
| [waiter waitForEnterCount:1 exitCount:1]; |
| EXPECT_EQ(1, [waiter enterCount]); |
| EXPECT_EQ(1, [waiter exitCount]); |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| } |
| |
| // Test that Widget::Restore exits fullscreen. |
| TEST_F(BridgedNativeWidgetUITest, FullscreenRestore) { |
| base::scoped_nsobject<NSWindowFullscreenNotificationWaiter> waiter( |
| [[NSWindowFullscreenNotificationWaiter alloc] |
| initWithWindow:test_window()]); |
| |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| const gfx::Rect restored_bounds = widget_->GetRestoredBounds(); |
| EXPECT_FALSE(restored_bounds.IsEmpty()); |
| |
| widget_->SetFullscreen(true); |
| EXPECT_TRUE(widget_->IsFullscreen()); |
| [waiter waitForEnterCount:1 exitCount:0]; |
| |
| widget_->Restore(); |
| EXPECT_FALSE(widget_->IsFullscreen()); |
| [waiter waitForEnterCount:1 exitCount:1]; |
| EXPECT_EQ(restored_bounds, widget_->GetRestoredBounds()); |
| } |
| |
| namespace { |
| |
| // This is used to wait for reposted events to be seen. We can't just use |
| // RunPendingMessages() because CGEventPost might not be synchronous. |
| class HitTestBridgedNativeWidget : public BridgedNativeWidget { |
| public: |
| explicit HitTestBridgedNativeWidget(NativeWidgetMac* widget) |
| : BridgedNativeWidget(widget) {} |
| |
| // BridgedNativeWidget: |
| bool ShouldRepostPendingLeftMouseDown(NSEvent* event) override { |
| did_repost_ = BridgedNativeWidget::ShouldRepostPendingLeftMouseDown(event); |
| |
| if (run_loop_) |
| run_loop_->Quit(); |
| |
| return did_repost_; |
| } |
| |
| void WaitForShouldRepost() { |
| base::RunLoop run_loop; |
| run_loop_ = &run_loop; |
| run_loop.Run(); |
| run_loop_ = nullptr; |
| } |
| |
| bool IsDraggable() { return [ns_view() mouseDownCanMoveWindow]; } |
| bool did_repost() { return did_repost_; } |
| |
| private: |
| base::RunLoop* run_loop_ = nullptr; |
| bool did_repost_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(HitTestBridgedNativeWidget); |
| }; |
| |
| // This is used to return a customized result to NonClientHitTest. |
| class HitTestNonClientFrameView : public NativeFrameView { |
| public: |
| explicit HitTestNonClientFrameView(Widget* widget) |
| : NativeFrameView(widget), hit_test_result_(HTNOWHERE) {} |
| |
| // NonClientFrameView overrides: |
| int NonClientHitTest(const gfx::Point& point) override { |
| return hit_test_result_; |
| } |
| |
| void set_hit_test_result(int component) { hit_test_result_ = component; } |
| |
| private: |
| int hit_test_result_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HitTestNonClientFrameView); |
| }; |
| |
| // This is used to change whether the Widget is resizable. |
| class HitTestWidgetDelegate : public views::WidgetDelegate { |
| public: |
| explicit HitTestWidgetDelegate(views::Widget* widget) : widget_(widget) {} |
| |
| void set_can_resize(bool can_resize) { |
| can_resize_ = can_resize; |
| widget_->OnSizeConstraintsChanged(); |
| } |
| |
| // views::WidgetDelegate: |
| bool CanResize() const override { return can_resize_; } |
| views::Widget* GetWidget() override { return widget_; } |
| views::Widget* GetWidget() const override { return widget_; } |
| |
| private: |
| views::Widget* widget_; |
| bool can_resize_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(HitTestWidgetDelegate); |
| }; |
| |
| void WaitForEvent(NSUInteger mask) { |
| // Pointer because the handler block captures local variables by copying. |
| base::RunLoop run_loop; |
| base::RunLoop* run_loop_ref = &run_loop; |
| id monitor = [NSEvent |
| addLocalMonitorForEventsMatchingMask:mask |
| handler:^NSEvent*(NSEvent* ns_event) { |
| run_loop_ref->Quit(); |
| return ns_event; |
| }]; |
| run_loop.Run(); |
| [NSEvent removeMonitor:monitor]; |
| } |
| |
| } // namespace |
| |
| // This is used to inject test versions of NativeFrameView and |
| // BridgedNativeWidget. |
| class HitTestNativeWidgetMac : public NativeWidgetMac { |
| public: |
| HitTestNativeWidgetMac(internal::NativeWidgetDelegate* delegate, |
| NativeFrameView* native_frame_view) |
| : NativeWidgetMac(delegate), native_frame_view_(native_frame_view) { |
| NativeWidgetMac::bridge_.reset(new HitTestBridgedNativeWidget(this)); |
| } |
| |
| HitTestBridgedNativeWidget* bridge() { |
| return static_cast<HitTestBridgedNativeWidget*>( |
| NativeWidgetMac::bridge_.get()); |
| } |
| |
| // internal::NativeWidgetPrivate: |
| NonClientFrameView* CreateNonClientFrameView() override { |
| return native_frame_view_; |
| } |
| |
| private: |
| // Owned by Widget. |
| NativeFrameView* native_frame_view_; |
| |
| DISALLOW_COPY_AND_ASSIGN(HitTestNativeWidgetMac); |
| }; |
| |
| // Flaky on macOS 10.12. See http://crbug.com/767299. |
| TEST_F(BridgedNativeWidgetUITest, DISABLED_HitTest) { |
| Widget widget; |
| HitTestNonClientFrameView* frame_view = |
| new HitTestNonClientFrameView(&widget); |
| test::HitTestNativeWidgetMac* native_widget = |
| new test::HitTestNativeWidgetMac(&widget, frame_view); |
| HitTestWidgetDelegate* widget_delegate = new HitTestWidgetDelegate(&widget); |
| Widget::InitParams init_params = |
| CreateParams(Widget::InitParams::TYPE_WINDOW); |
| init_params.native_widget = native_widget; |
| init_params.delegate = widget_delegate; |
| init_params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; |
| init_params.bounds = gfx::Rect(100, 200, 400, 300); |
| widget.Init(init_params); |
| |
| WidgetActivationWaiter activation_waiter(&widget, true); |
| widget.Show(); |
| activation_waiter.Wait(); |
| |
| // Points inside the resize area. |
| const NSPoint bottom_right_point = {398, 2}; |
| const NSPoint right_of_bottom_right = {398 + 10, 2}; |
| |
| NSWindow* window = widget.GetNativeWindow(); |
| |
| EXPECT_FALSE([window ignoresMouseEvents]); |
| // OSX uses both the alpha value of the window and the underlying CALayer to |
| // decide whether to send mouse events to window, in case [window |
| // ignoresMouseEvent] is not explicitly initialized. Since, no frames are |
| // drawn during tests and the underlying CALayer has a transparent background, |
| // explicitly call setIgnoresMouseEvents: to ensure the window receives the |
| // mouse events. |
| [window setIgnoresMouseEvents:NO]; |
| |
| HitTestBridgedNativeWidget* bridge = native_widget->bridge(); |
| |
| const bool using_drag_event_monitor = |
| BridgedNativeWidget::ShouldUseDragEventMonitor(); |
| |
| // Dragging the window should work. |
| frame_view->set_hit_test_result(HTCAPTION); |
| { |
| EXPECT_EQ(100, [window frame].origin.x); |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowWillMoveNotification]); |
| NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( |
| NSMakePoint(20, 20), window); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]); |
| if (using_drag_event_monitor) { |
| bridge->WaitForShouldRepost(); |
| EXPECT_TRUE(bridge->did_repost()); |
| EXPECT_TRUE(bridge->IsDraggable()); |
| bridge->WaitForShouldRepost(); |
| EXPECT_FALSE(bridge->did_repost()); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| } else { |
| WaitForEvent(NSLeftMouseDownMask); |
| } |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> did_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowDidMoveNotification]); |
| NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| NSMakePoint(30, 30), NSLeftMouseDragged, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]); |
| WaitForEvent(NSLeftMouseDraggedMask); |
| // NSWindowWillMoveNotification should have been observed by the time the |
| // mouse drag event is received. |
| EXPECT_EQ(1, [will_move_observer notificationCount]); |
| EXPECT_TRUE([did_move_observer wait]); |
| EXPECT_EQ(110, [window frame].origin.x); |
| |
| NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| NSMakePoint(20, 20), NSLeftMouseUp, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]); |
| WaitForEvent(NSLeftMouseUpMask); |
| EXPECT_EQ(110, [window frame].origin.x); |
| } |
| |
| // Dragging in the resize area works since the widget is not resizable. |
| { |
| EXPECT_EQ(110, [window frame].origin.x); |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowWillMoveNotification]); |
| NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( |
| bottom_right_point, window); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]); |
| if (using_drag_event_monitor) { |
| bridge->WaitForShouldRepost(); |
| EXPECT_TRUE(bridge->did_repost()); |
| EXPECT_TRUE(bridge->IsDraggable()); |
| bridge->WaitForShouldRepost(); |
| EXPECT_FALSE(bridge->did_repost()); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| } else { |
| WaitForEvent(NSLeftMouseDownMask); |
| } |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> did_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowDidMoveNotification]); |
| NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| right_of_bottom_right, NSLeftMouseDragged, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]); |
| WaitForEvent(NSLeftMouseDraggedMask); |
| EXPECT_EQ(1, [will_move_observer notificationCount]); |
| EXPECT_TRUE([did_move_observer wait]); |
| EXPECT_EQ(120, [window frame].origin.x); |
| |
| NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| bottom_right_point, NSLeftMouseUp, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]); |
| WaitForEvent(NSLeftMouseUpMask); |
| EXPECT_EQ(120, [window frame].origin.x); |
| } |
| |
| // If the widget is resizable, dragging in the resize area should not repost |
| // (and should resize). |
| widget_delegate->set_can_resize(true); |
| { |
| EXPECT_EQ(400, [window frame].size.width); |
| |
| NSUInteger x = [window frame].origin.x; |
| NSUInteger y = [window frame].origin.y; |
| |
| // Enqueue all mouse events first because AppKit will run its own loop to |
| // consume them. |
| base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowWillMoveNotification]); |
| NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( |
| bottom_right_point, window); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]); |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> did_resize_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowDidResizeNotification]); |
| NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPoint( |
| NSMakePoint(x + 408, y + 2), NSLeftMouseDragged, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]); |
| |
| NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPoint( |
| NSMakePoint(x + 408, y + 2), NSLeftMouseUp, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]); |
| |
| if (using_drag_event_monitor) { |
| // The only event observed by us is the original mouse-down. It should not |
| // be reposted. |
| bridge->WaitForShouldRepost(); |
| EXPECT_FALSE(bridge->did_repost()); |
| EXPECT_FALSE(bridge->IsDraggable()); |
| } |
| |
| EXPECT_TRUE([did_resize_observer wait]); |
| EXPECT_EQ(0, [will_move_observer notificationCount]); |
| EXPECT_EQ(410, [window frame].size.width); |
| |
| // Origin is unchanged. |
| EXPECT_EQ(x, [window frame].origin.x); |
| EXPECT_EQ(y, [window frame].origin.y); |
| } |
| |
| // Mouse-downs on the window controls should not be intercepted. |
| { |
| EXPECT_EQ(120, [window frame].origin.x); |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowWillMoveNotification]); |
| base::scoped_nsobject<WindowedNSNotificationObserver> |
| did_miniaturize_observer([[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowDidMiniaturizeNotification]); |
| |
| // Position this on the minimize button. |
| NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( |
| NSMakePoint(30, 290), window); |
| CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]); |
| |
| NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| NSMakePoint(30, 290), NSLeftMouseUp, window, 0); |
| EXPECT_FALSE([window isMiniaturized]); |
| CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]); |
| [did_miniaturize_observer wait]; |
| EXPECT_EQ(0, [will_move_observer notificationCount]); |
| EXPECT_TRUE([window isMiniaturized]); |
| [window deminiaturize:nil]; |
| |
| // Position unchanged. |
| EXPECT_EQ(120, [window frame].origin.x); |
| } |
| |
| // Non-draggable areas should do nothing. |
| frame_view->set_hit_test_result(HTCLIENT); |
| { |
| EXPECT_EQ(120, [window frame].origin.x); |
| |
| base::scoped_nsobject<WindowedNSNotificationObserver> will_move_observer( |
| [[WindowedNSNotificationObserver alloc] |
| initForNotification:NSWindowWillMoveNotification]); |
| NSEvent* mouse_down = cocoa_test_event_utils::LeftMouseDownAtPointInWindow( |
| NSMakePoint(20, 20), window); |
| CGEventPost(kCGSessionEventTap, [mouse_down CGEvent]); |
| WaitForEvent(NSLeftMouseDownMask); |
| |
| NSEvent* mouse_drag = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| NSMakePoint(30, 30), NSLeftMouseDragged, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_drag CGEvent]); |
| WaitForEvent(NSLeftMouseDraggedMask); |
| EXPECT_EQ(0, [will_move_observer notificationCount]); |
| EXPECT_EQ(120, [window frame].origin.x); |
| |
| NSEvent* mouse_up = cocoa_test_event_utils::MouseEventAtPointInWindow( |
| NSMakePoint(30, 30), NSLeftMouseUp, window, 0); |
| CGEventPost(kCGSessionEventTap, [mouse_up CGEvent]); |
| WaitForEvent(NSLeftMouseUpMask); |
| EXPECT_EQ(120, [window frame].origin.x); |
| } |
| } |
| |
| } // namespace test |
| } // namespace views |