blob: cbe92c1aa3557f9ed013db5621fef984b40ef67d [file] [log] [blame]
// 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