blob: cac172517d89d336f42f0a86ac0b7957f3af3cda [file] [log] [blame]
// Copyright 2016 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.
#include <memory>
#include <string>
#include <utility>
#include "base/memory/ref_counted.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/chooser_bubble_testapi.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/browser/usb/usb_chooser_controller.h"
#include "chrome/browser/usb/web_usb_chooser.h"
#include "chrome/browser/usb/web_usb_service_impl.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "services/service_manager/public/cpp/binder_registry.h"
namespace blink {
namespace mojom {
class WebUsbService;
}
} // namespace blink
using content::RenderFrameHost;
using device::FakeUsbDeviceManager;
using device::mojom::UsbDeviceInfoPtr;
using device::mojom::UsbDeviceManager;
namespace {
class FakeChooserView : public permissions::ChooserController::View {
public:
explicit FakeChooserView(
std::unique_ptr<permissions::ChooserController> controller)
: controller_(std::move(controller)) {
controller_->set_view(this);
}
~FakeChooserView() override { controller_->set_view(nullptr); }
void OnOptionsInitialized() override {
if (controller_->NumOptions())
controller_->Select({0});
else
controller_->Cancel();
delete this;
}
void OnOptionAdded(size_t index) override { NOTREACHED(); }
void OnOptionRemoved(size_t index) override { NOTREACHED(); }
void OnOptionUpdated(size_t index) override { NOTREACHED(); }
void OnAdapterEnabledChanged(bool enabled) override { NOTREACHED(); }
void OnRefreshStateChanged(bool refreshing) override { NOTREACHED(); }
private:
std::unique_ptr<permissions::ChooserController> controller_;
DISALLOW_COPY_AND_ASSIGN(FakeChooserView);
};
class FakeUsbChooser : public WebUsbChooser {
public:
explicit FakeUsbChooser(RenderFrameHost* render_frame_host)
: WebUsbChooser(render_frame_host) {}
~FakeUsbChooser() override {}
void ShowChooser(std::unique_ptr<UsbChooserController> controller) override {
// Device list initialization in UsbChooserController may completed before
// having a valid view in which case OnOptionsInitialized() has no chance to
// be triggered, so select the first option directly if options are ready.
if (controller->NumOptions())
controller->Select({0});
else
new FakeChooserView(std::move(controller));
}
base::WeakPtr<WebUsbChooser> GetWeakPtr() override {
return weak_factory_.GetWeakPtr();
}
private:
base::WeakPtrFactory<FakeUsbChooser> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(FakeUsbChooser);
};
class TestContentBrowserClient : public ChromeContentBrowserClient {
public:
TestContentBrowserClient() {}
~TestContentBrowserClient() override {}
// ChromeContentBrowserClient:
void CreateWebUsbService(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) override {
if (use_real_chooser_) {
ChromeContentBrowserClient::CreateWebUsbService(render_frame_host,
std::move(receiver));
} else {
usb_chooser_ = std::make_unique<FakeUsbChooser>(render_frame_host);
web_usb_service_ = std::make_unique<WebUsbServiceImpl>(
render_frame_host, usb_chooser_->GetWeakPtr());
web_usb_service_->BindReceiver(std::move(receiver));
}
}
void UseRealChooser() { use_real_chooser_ = true; }
private:
bool use_real_chooser_ = false;
std::unique_ptr<WebUsbServiceImpl> web_usb_service_;
std::unique_ptr<WebUsbChooser> usb_chooser_;
DISALLOW_COPY_AND_ASSIGN(TestContentBrowserClient);
};
class WebUsbTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
AddFakeDevice("123456");
// Connect with the FakeUsbDeviceManager.
mojo::PendingRemote<UsbDeviceManager> device_manager;
device_manager_.AddReceiver(
device_manager.InitWithNewPipeAndPassReceiver());
UsbChooserContextFactory::GetForProfile(browser()->profile())
->SetDeviceManagerForTesting(std::move(device_manager));
original_content_browser_client_ =
content::SetBrowserClientForTesting(&test_content_browser_client_);
GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html");
ui_test_utils::NavigateToURL(browser(), url);
origin_ = url.GetOrigin();
RenderFrameHost* render_frame_host =
browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
EXPECT_EQ(origin_, render_frame_host->GetLastCommittedOrigin().GetURL());
}
void TearDown() override {
content::SetBrowserClientForTesting(original_content_browser_client_);
}
void AddFakeDevice(const std::string& serial_number) {
DCHECK(!fake_device_info_);
fake_device_info_ = device_manager_.CreateAndAddDevice(
0, 0, "Test Manufacturer", "Test Device", serial_number);
}
void RemoveFakeDevice() {
DCHECK(fake_device_info_);
device_manager_.RemoveDevice(fake_device_info_->guid);
fake_device_info_ = nullptr;
}
const GURL& origin() { return origin_; }
void UseRealChooser() { test_content_browser_client_.UseRealChooser(); }
private:
FakeUsbDeviceManager device_manager_;
UsbDeviceInfoPtr fake_device_info_;
TestContentBrowserClient test_content_browser_client_;
content::ContentBrowserClient* original_content_browser_client_;
GURL origin_;
};
IN_PROC_BROWSER_TEST_F(WebUsbTest, RequestAndGetDevices) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(content::ListValueOf(), content::EvalJs(web_contents,
R"((async () => {
let devices = await navigator.usb.getDevices();
return devices.map(device => device.serialNumber);
})())"));
EXPECT_EQ("123456", content::EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
EXPECT_EQ(content::ListValueOf("123456"), content::EvalJs(web_contents,
R"((async () => {
let devices = await navigator.usb.getDevices();
return devices.map(device => device.serialNumber);
})())"));
}
IN_PROC_BROWSER_TEST_F(WebUsbTest, RequestDeviceWithGuardBlocked) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
map->SetContentSettingDefaultScope(origin(), origin(),
ContentSettingsType::USB_GUARD,
CONTENT_SETTING_BLOCK);
EXPECT_EQ("NotFoundError: No device selected.",
content::EvalJs(web_contents,
R"((async () => {
try {
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return "Expected error, got success.";
} catch (e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(WebUsbTest, AddRemoveDevice) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ("123456", content::EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var removedPromise = new Promise(resolve => {
navigator.usb.addEventListener('disconnect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
RemoveFakeDevice();
EXPECT_EQ("123456", content::EvalJs(web_contents, "removedPromise"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var addedPromise = new Promise(resolve => {
navigator.usb.addEventListener('connect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
AddFakeDevice("123456");
EXPECT_EQ("123456", content::EvalJs(web_contents, "addedPromise"));
}
IN_PROC_BROWSER_TEST_F(WebUsbTest, AddRemoveDeviceEphemeral) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
// Replace the default mock device with one that has no serial number.
RemoveFakeDevice();
AddFakeDevice("");
EXPECT_EQ("", content::EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var removedPromise = new Promise(resolve => {
navigator.usb.addEventListener('disconnect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
RemoveFakeDevice();
EXPECT_EQ("", content::EvalJs(web_contents, "removedPromise"));
}
IN_PROC_BROWSER_TEST_F(WebUsbTest, NavigateWithChooserCrossOrigin) {
UseRealChooser();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver observer(
web_contents, 1 /* number_of_navigations */,
content::MessageLoopRunner::QuitMode::DEFERRED);
auto waiter = test::ChooserBubbleUiWaiter::Create();
EXPECT_TRUE(content::ExecJs(web_contents,
"navigator.usb.requestDevice({ filters: [] })",
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Wait for the chooser to be displayed before navigating to avoid a race
// between the two IPCs.
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_shown());
EXPECT_TRUE(content::ExecJs(web_contents,
"document.location.href = 'https://google.com'"));
observer.Wait();
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_closed());
EXPECT_EQ(GURL("https://google.com"), web_contents->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_F(WebUsbTest, ShowChooserInBackgroundTab) {
UseRealChooser();
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
// Create a new foreground tab that covers |web_contents|.
GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html");
ui_test_utils::NavigateToURLWithDisposition(
browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
// Try to show the chooser in the background tab.
EXPECT_EQ("NotFoundError: No device selected.",
content::EvalJs(web_contents,
R"((async () => {
try {
await navigator.usb.requestDevice({ filters: [] });
return "Expected error, got success.";
} catch (e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
class WebUsbPrerenderinBrowserTest : public WebUsbTest {
public:
WebUsbPrerenderinBrowserTest()
: prerender_helper_(
base::BindRepeating(&WebUsbPrerenderinBrowserTest::web_contents,
base::Unretained(this))) {}
~WebUsbPrerenderinBrowserTest() override = default;
content::test::PrerenderTestHelper* prerender_helper() {
return &prerender_helper_;
}
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
void OnJsTestExecutionDone(base::Value value) { result_ = std::move(value); }
bool HasResult() { return !!result_; }
private:
absl::optional<base::Value> result_;
content::test::PrerenderTestHelper prerender_helper_;
};
IN_PROC_BROWSER_TEST_F(WebUsbPrerenderinBrowserTest, ShowChooserInPrerenderin) {
// Loads a page in the prerendering.
GURL prerender_url = embedded_test_server()->GetURL(
"localhost", "/simple_page.html?prerendering");
const int host_id = prerender_helper()->AddPrerender(prerender_url);
content::RenderFrameHost* prerender_rfh =
prerender_helper()->GetPrerenderedMainFrameHost(host_id);
EXPECT_FALSE(HasResult());
prerender_rfh->ExecuteJavaScriptForTests(
u"((async () => {"
u" let devices = await navigator.usb.getDevices();"
u" return devices.map(device => device.serialNumber);"
u"})())",
base::BindOnce(&WebUsbPrerenderinBrowserTest::OnJsTestExecutionDone,
base::Unretained(this)));
// Activate the prerendered page.
prerender_helper()->NavigatePrimaryPage(prerender_url);
EXPECT_TRUE(HasResult());
}
} // namespace