| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This class runs functional tests for lens overlay. These tests spin up a full |
| // web browser, but allow for inspection and modification of internal state of |
| // LensOverlayController and other business-logic classes. |
| |
| #include "chrome/browser/ui/lens/lens_overlay_controller.h" |
| |
| #include "base/test/run_until.h" |
| #include "chrome/browser/lens/core/mojom/geometry.mojom.h" |
| #include "chrome/browser/lens/core/mojom/lens.mojom.h" |
| #include "chrome/browser/lens/core/mojom/overlay_object.mojom.h" |
| #include "chrome/browser/lens/lens_overlay/lens_overlay_url_builder.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/side_panel/side_panel_entry_id.h" |
| #include "chrome/browser/ui/tabs/tab_features.h" |
| #include "chrome/browser/ui/tabs/tab_model.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/views/side_panel/lens/lens_overlay_side_panel_coordinator.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_coordinator.h" |
| #include "chrome/browser/ui/views/side_panel/side_panel_util.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/lens/lens_features.h" |
| #include "components/permissions/test/permission_request_observer.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/hit_test_region_observer.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/view_utils.h" |
| |
| namespace { |
| |
| constexpr char kDocumentWithNamedElement[] = "/select.html"; |
| |
| using State = LensOverlayController::State; |
| |
| constexpr char kRequestNotificationsScript[] = R"( |
| new Promise(resolve => { |
| Notification.requestPermission().then(function (permission) { |
| resolve(permission); |
| }); |
| }) |
| )"; |
| |
| class LensOverlayPageFake : public lens::mojom::LensPage { |
| public: |
| void ObjectsReceived( |
| std::vector<lens::mojom::OverlayObjectPtr> objects) override { |
| last_received_objects_ = std::move(objects); |
| } |
| |
| void TextReceived(lens::mojom::TextPtr text) override { |
| last_received_text_ = std::move(text); |
| } |
| |
| std::vector<lens::mojom::OverlayObjectPtr> last_received_objects_; |
| lens::mojom::TextPtr last_received_text_; |
| }; |
| |
| // Stubs out network requests and mojo calls. |
| class LensOverlayControllerFake : public LensOverlayController { |
| public: |
| explicit LensOverlayControllerFake(tabs::TabModel* tab_model) |
| : LensOverlayController(tab_model) {} |
| |
| void BindOverlay(mojo::PendingReceiver<lens::mojom::LensPageHandler> receiver, |
| mojo::PendingRemote<lens::mojom::LensPage> page) override { |
| // Set up the fake overlay page to intercept the mojo call. |
| LensOverlayController::BindOverlay( |
| std::move(receiver), |
| fake_overlay_page_receiver_.BindNewPipeAndPassRemote()); |
| } |
| |
| void FlushForTesting() { fake_overlay_page_receiver_.FlushForTesting(); } |
| |
| const LensOverlayPageFake& GetFakeLensOverlayPage() { |
| return fake_overlay_page_; |
| } |
| |
| LensOverlayPageFake fake_overlay_page_; |
| mojo::Receiver<lens::mojom::LensPage> fake_overlay_page_receiver_{ |
| &fake_overlay_page_}; |
| }; |
| |
| class TabFeaturesFake : public tabs::TabFeatures { |
| public: |
| TabFeaturesFake() = default; |
| |
| protected: |
| std::unique_ptr<LensOverlayController> CreateLensController( |
| tabs::TabModel* tab) override { |
| return std::make_unique<LensOverlayControllerFake>(tab); |
| } |
| }; |
| |
| class LensOverlayControllerBrowserTest : public InProcessBrowserTest { |
| protected: |
| LensOverlayControllerBrowserTest() { |
| tabs::TabFeatures::ReplaceTabFeaturesForTesting(base::BindRepeating( |
| &LensOverlayControllerBrowserTest::CreateTabFeatures, |
| base::Unretained(this))); |
| } |
| |
| void SetUp() override { |
| ASSERT_TRUE(embedded_test_server()->InitializeAndListen()); |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| InProcessBrowserTest::SetUpOnMainThread(); |
| embedded_test_server()->StartAcceptingConnections(); |
| } |
| |
| ~LensOverlayControllerBrowserTest() override { |
| tabs::TabFeatures::ReplaceTabFeaturesForTesting(base::NullCallback()); |
| } |
| |
| std::unique_ptr<tabs::TabFeatures> CreateTabFeatures() { |
| return std::make_unique<TabFeaturesFake>(); |
| } |
| |
| content::WebContents* GetOverlayWebContents() { |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| raw_ptr<views::WebView> overlay_web_view = |
| views::AsViewClass<views::WebView>( |
| controller->GetOverlayWidgetForTesting() |
| ->GetContentsView() |
| ->children()[0]); |
| return overlay_web_view->GetWebContents(); |
| } |
| |
| void SimulateLeftClickDrag(gfx::Point from, gfx::Point to) { |
| auto* overlay_web_contents = GetOverlayWebContents(); |
| // We should wait for the main frame's hit-test data to be ready before |
| // sending the click event below to avoid flakiness. |
| content::WaitForHitTestData(overlay_web_contents->GetPrimaryMainFrame()); |
| content::SimulateMouseEvent(overlay_web_contents, |
| blink::WebInputEvent::Type::kMouseDown, |
| blink::WebMouseEvent::Button::kLeft, from); |
| content::SimulateMouseEvent(overlay_web_contents, |
| blink::WebInputEvent::Type::kMouseMove, |
| blink::WebMouseEvent::Button::kLeft, to); |
| content::SimulateMouseEvent(overlay_web_contents, |
| blink::WebInputEvent::Type::kMouseUp, |
| blink::WebMouseEvent::Button::kLeft, to); |
| content::RunUntilInputProcessed( |
| overlay_web_contents->GetRenderWidgetHostView()->GetRenderWidgetHost()); |
| } |
| |
| // Lens overlay takes a screenshot of the tab. In order to take a screenshot |
| // the tab must not be about:blank and must be painted. |
| void WaitForPaint() { |
| const GURL url = embedded_test_server()->GetURL(kDocumentWithNamedElement); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| ASSERT_TRUE(base::test::RunUntil([&]() { |
| return browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->contents() |
| ->CompletedFirstVisuallyNonEmptyPaint(); |
| })); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_{lens::features::kLensOverlay}; |
| }; |
| |
| // TODO(https://crbug.com/329708692): If this test flakes please disable and |
| // refile against the same bug. |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, CaptureScreenshot) { |
| WaitForPaint(); |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| |
| // Verify screenshot was captured and stored. |
| auto screenshot_bitmap = controller->current_screenshot(); |
| ASSERT_FALSE(screenshot_bitmap.empty()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, CreateAndLoadWebUI) { |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| |
| // Assert that the web view was created and loaded WebUI. |
| GURL webui_url(chrome::kChromeUILensUntrustedURL); |
| ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents())); |
| ASSERT_EQ(GetOverlayWebContents()->GetLastCommittedURL(), webui_url); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, ShowSidePanel) { |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| |
| // Now show the side panel. |
| controller->side_panel_coordinator()->RegisterEntryAndShow(); |
| |
| auto* coordinator = |
| SidePanelUtil::GetSidePanelCoordinatorForBrowser(browser()); |
| EXPECT_TRUE(coordinator->IsSidePanelShowing()); |
| EXPECT_EQ(coordinator->GetCurrentEntryId(), |
| SidePanelEntry::Id::kLensOverlayResults); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, |
| DelayPermissionsPrompt) { |
| // Navigate to a page so we can request permissions |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| |
| content::WebContents* contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| ASSERT_TRUE(contents); |
| |
| permissions::PermissionRequestObserver observer(contents); |
| |
| // Request permission in tab under overlay. |
| EXPECT_TRUE(content::ExecJs( |
| contents->GetPrimaryMainFrame(), kRequestNotificationsScript, |
| content::EvalJsOptions::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES)); |
| |
| // Verify no prompt was shown |
| observer.Wait(); |
| EXPECT_FALSE(observer.request_shown()); |
| |
| // Close overlay |
| controller->CloseUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOff; })); |
| |
| // Verify a prompt was shown |
| ASSERT_TRUE(base::test::RunUntil([&]() { return observer.request_shown(); })); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, |
| ShowSidePanelAfterManualRegionSelection) { |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents())); |
| |
| // Simulate mouse events on the overlay for drawing a manual region. |
| gfx::Point center = |
| GetOverlayWebContents()->GetContainerBounds().CenterPoint(); |
| gfx::Point off_center = gfx::Point(center); |
| off_center.Offset(100, 100); |
| SimulateLeftClickDrag(center, off_center); |
| |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlayAndResults; })); |
| auto* coordinator = |
| SidePanelUtil::GetSidePanelCoordinatorForBrowser(browser()); |
| // Expect the Lens Overlay results panel to open. |
| ASSERT_TRUE(coordinator->IsSidePanelShowing()); |
| EXPECT_EQ(coordinator->GetCurrentEntryId(), |
| SidePanelEntry::Id::kLensOverlayResults); |
| } |
| |
| // TODO(b/328294794): This browser test should be deleted / modified after text |
| // requests are implemented from mojo. |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, |
| ShowSidePanelAfterTextSelectionRequest) { |
| WaitForPaint(); |
| |
| std::string text_query = "Apples"; |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents())); |
| |
| // TODO(b/328294794): This function should be replaced when the text selection |
| // call from mojo is implemented. |
| controller->IssueTextSelectionRequestForTesting(text_query); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlayAndResults; })); |
| |
| // Expect the Lens Overlay results panel to open. |
| auto* coordinator = |
| SidePanelUtil::GetSidePanelCoordinatorForBrowser(browser()); |
| ASSERT_TRUE(coordinator->IsSidePanelShowing()); |
| EXPECT_EQ(coordinator->GetCurrentEntryId(), |
| SidePanelEntry::Id::kLensOverlayResults); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, |
| HandleStartQueryResponse) { |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents())); |
| |
| // Set up fake test objects to send to controller. |
| auto test_object = lens::mojom::OverlayObject::New(); |
| test_object->id = "unique_id"; |
| test_object->geometry = lens::mojom::Geometry::New(); |
| test_object->geometry->bounding_box = lens::mojom::CenterRotatedBox::New(); |
| test_object->geometry->bounding_box->box = gfx::RectF(0.1, 0.1, 0.8, 0.8); |
| |
| std::vector<lens::mojom::OverlayObjectPtr> test_objects; |
| test_objects.push_back(test_object->Clone()); |
| |
| auto test_text = lens::mojom::Text::New(); |
| test_text->content_language = "es"; |
| test_text->text_layout = lens::mojom::TextLayout::New(); |
| |
| // Call the response callback and flush the receiver. |
| controller->HandleStartQueryResponse(std::move(test_objects), |
| test_text->Clone()); |
| |
| auto* fake_controller = static_cast<LensOverlayControllerFake*>(controller); |
| ASSERT_TRUE(fake_controller); |
| fake_controller->FlushForTesting(); |
| |
| EXPECT_FALSE( |
| fake_controller->GetFakeLensOverlayPage().last_received_objects_.empty()); |
| EXPECT_TRUE(test_object->Equals( |
| *fake_controller->GetFakeLensOverlayPage().last_received_objects_[0])); |
| EXPECT_TRUE(test_text->Equals( |
| *fake_controller->GetFakeLensOverlayPage().last_received_text_)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(LensOverlayControllerBrowserTest, |
| HandleStartQueryResponse_NoObjectsNoText) { |
| WaitForPaint(); |
| |
| // State should start in off. |
| auto* controller = browser() |
| ->tab_strip_model() |
| ->GetActiveTab() |
| ->tab_features() |
| ->lens_overlay_controller(); |
| ASSERT_EQ(controller->state(), State::kOff); |
| |
| // Showing UI should eventually result in overlay state. |
| controller->ShowUI(); |
| ASSERT_TRUE(base::test::RunUntil( |
| [&]() { return controller->state() == State::kOverlay; })); |
| ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents())); |
| |
| // Call the response callback and flush the receiver. |
| controller->HandleStartQueryResponse({}, nullptr); |
| auto* fake_controller = static_cast<LensOverlayControllerFake*>(controller); |
| ASSERT_TRUE(fake_controller); |
| fake_controller->FlushForTesting(); |
| |
| EXPECT_TRUE( |
| fake_controller->GetFakeLensOverlayPage().last_received_objects_.empty()); |
| EXPECT_FALSE(fake_controller->GetFakeLensOverlayPage().last_received_text_); |
| } |
| |
| } // namespace |