| // Copyright 2016 The Chromium Authors |
| // 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 <string_view> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/gmock_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chrome_content_browser_client.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/device_notifications/device_pinned_notification_renderer.h" |
| #include "chrome/browser/device_notifications/device_status_icon_renderer.h" |
| #include "chrome/browser/notifications/notification_display_service_tester.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/ui/web_applications/test/isolated_web_app_test_utils.h" |
| #include "chrome/browser/usb/chrome_usb_delegate.h" |
| #include "chrome/browser/usb/usb_browser_test_utils.h" |
| #include "chrome/browser/usb/usb_chooser_context.h" |
| #include "chrome/browser/usb/usb_chooser_context_factory.h" |
| #include "chrome/browser/usb/usb_chooser_controller.h" |
| #include "chrome/browser/usb/usb_pinned_notification.h" |
| #include "chrome/browser/usb/usb_status_icon.h" |
| #include "chrome/browser/usb/web_usb_chooser.h" |
| #include "chrome/browser/usb/web_usb_histograms.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/mixin_based_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 "components/prefs/pref_service.h" |
| #include "content/public/browser/console_message.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "content/public/browser/service_worker_context_observer.h" |
| #include "content/public/browser/service_worker_running_info.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/usb_chooser.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.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/service_worker_test_helpers.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "extensions/buildflags/buildflags.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/device/public/mojom/usb_enumeration_options.mojom.h" |
| #include "services/service_manager/public/cpp/binder_registry.h" |
| #include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h" |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "base/test/test_future.h" |
| #include "base/test/values_test_util.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/content_settings/core/common/pref_names.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/test/regular_logged_in_browser_test_mixin.h" |
| #include "components/account_id/account_id.h" |
| #include "components/account_id/account_id_literal.h" // nogncheck |
| #include "components/user_manager/user_manager.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| namespace { |
| |
| using ::base::test::TestFuture; |
| using ::content::JsReplace; |
| using ::extensions::Extension; |
| using ::extensions::ExtensionId; |
| using ::extensions::TestExtensionDir; |
| using ::testing::Return; |
| |
| const char kTestExtensionId[] = "iegclhlplifhodhkoafiokenjoapiobj"; |
| // Key for extension id `kTestExtensionId`. |
| constexpr const char kTestExtensionKey[] = |
| "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P" |
| "Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf" |
| "qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv" |
| "7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i" |
| "L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz" |
| "afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB"; |
| constexpr uint8_t kUsbPrinterClass = 7; |
| constexpr char kNonAppHost[] = "nonapp.com"; |
| constexpr char kNonAppHost2[] = "nonapp2.com"; |
| constexpr char OpenAndClaimDeviceScript[] = R"((async () => { |
| try { |
| const devices = await navigator.usb.getDevices(); |
| const device = devices[0]; |
| await device.open(); |
| await device.selectConfiguration(1); |
| await device.claimInterface(0); |
| return "Success"; |
| } catch (e) { |
| return e.message; |
| } |
| })();)"; |
| |
| auto FailedWithSubstr(std::string_view substr) { |
| return content::EvalJsResult::ErrorIs(testing::HasSubstr(substr)); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) && BUILDFLAG(IS_CHROMEOS) |
| constexpr auto kManagedUserAccountId = |
| AccountId::Literal::FromUserEmailGaiaId("example@example.com", |
| GaiaId::Literal("12345")); |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) && BUILDFLAG(IS_CHROMEOS) |
| |
| // Observer for an extension service worker events like start, activated, and |
| // stop. |
| class TestServiceWorkerContextObserver |
| : public content::ServiceWorkerContextObserver { |
| public: |
| TestServiceWorkerContextObserver(content::ServiceWorkerContext* context, |
| const ExtensionId& extension_id) |
| : extension_url_(Extension::GetBaseURLFromExtensionId(extension_id)) { |
| scoped_observation_.Observe(context); |
| } |
| |
| TestServiceWorkerContextObserver(const TestServiceWorkerContextObserver&) = |
| delete; |
| TestServiceWorkerContextObserver& operator=( |
| const TestServiceWorkerContextObserver&) = delete; |
| |
| ~TestServiceWorkerContextObserver() override = default; |
| |
| void WaitForWorkerStart() { |
| started_run_loop_.Run(); |
| EXPECT_TRUE(running_version_id_.has_value()); |
| } |
| |
| void WaitForWorkerActivated() { |
| activated_run_loop_.Run(); |
| EXPECT_TRUE(running_version_id_.has_value()); |
| } |
| |
| void WaitForWorkerStop() { |
| stopped_run_loop_.Run(); |
| EXPECT_EQ(running_version_id_, std::nullopt); |
| } |
| |
| int64_t GetServiceWorkerVersionId() { return running_version_id_.value(); } |
| |
| private: |
| // ServiceWorkerContextObserver: |
| void OnVersionStartedRunning( |
| int64_t version_id, |
| const content::ServiceWorkerRunningInfo& running_info) override { |
| if (running_info.scope != extension_url_) { |
| return; |
| } |
| running_version_id_ = version_id; |
| started_run_loop_.Quit(); |
| } |
| |
| void OnVersionActivated(int64_t version_id, const GURL& scope) override { |
| if (running_version_id_ != version_id) { |
| return; |
| } |
| activated_run_loop_.Quit(); |
| } |
| |
| void OnVersionStoppedRunning(int64_t version_id) override { |
| if (running_version_id_ != version_id) { |
| return; |
| } |
| stopped_run_loop_.Quit(); |
| running_version_id_ = std::nullopt; |
| } |
| |
| void OnDestruct(content::ServiceWorkerContext* context) override { |
| ASSERT_TRUE(scoped_observation_.IsObserving()); |
| scoped_observation_.Reset(); |
| } |
| |
| base::RunLoop started_run_loop_; |
| base::RunLoop activated_run_loop_; |
| base::RunLoop stopped_run_loop_; |
| std::optional<int64_t> running_version_id_; |
| base::ScopedObservation<content::ServiceWorkerContext, |
| content::ServiceWorkerContextObserver> |
| scoped_observation_{this}; |
| GURL extension_url_; |
| }; |
| |
| class TestServiceWorkerConsoleObserver |
| : public content::ServiceWorkerContextObserver { |
| public: |
| explicit TestServiceWorkerConsoleObserver( |
| content::BrowserContext* browser_context) { |
| content::StoragePartition* partition = |
| browser_context->GetDefaultStoragePartition(); |
| scoped_observation_.Observe(partition->GetServiceWorkerContext()); |
| } |
| ~TestServiceWorkerConsoleObserver() override = default; |
| |
| TestServiceWorkerConsoleObserver(const TestServiceWorkerConsoleObserver&) = |
| delete; |
| TestServiceWorkerConsoleObserver& operator=( |
| const TestServiceWorkerConsoleObserver&) = delete; |
| |
| using Message = content::ConsoleMessage; |
| const std::vector<Message>& messages() const { return messages_; } |
| |
| void WaitForMessages() { run_loop_.Run(); } |
| |
| private: |
| // ServiceWorkerContextObserver: |
| void OnReportConsoleMessage(int64_t version_id, |
| const GURL& scope, |
| const Message& message) override { |
| messages_.push_back(message); |
| run_loop_.Quit(); |
| } |
| |
| base::RunLoop run_loop_; |
| std::vector<Message> messages_; |
| base::ScopedObservation<content::ServiceWorkerContext, |
| content::ServiceWorkerContextObserver> |
| scoped_observation_{this}; |
| }; |
| |
| class ChromeWebUsbTest : 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<device::mojom::UsbDeviceManager> device_manager; |
| device_manager_.AddReceiver( |
| device_manager.InitWithNewPipeAndPassReceiver()); |
| UsbChooserContextFactory::GetForProfile(browser()->profile()) |
| ->SetDeviceManagerForTesting(std::move(device_manager)); |
| |
| test_content_browser_client_.SetAsBrowserClient(); |
| |
| GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html"); |
| EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| origin_ = url.DeprecatedGetOriginAsURL(); |
| |
| content::RenderFrameHost* render_frame_host = browser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetPrimaryMainFrame(); |
| EXPECT_EQ(origin_, render_frame_host->GetLastCommittedOrigin().GetURL()); |
| } |
| |
| void TearDownOnMainThread() override { |
| test_content_browser_client_.UnsetAsBrowserClient(); |
| } |
| |
| void AddFakeDevice(const std::string& serial_number) { |
| ASSERT_TRUE(!fake_device_info_); |
| fake_device_info_ = device_manager_.CreateAndAddDevice( |
| 0, 0, "Test Manufacturer", "Test Device", serial_number); |
| } |
| |
| void RemoveFakeDevice() { |
| ASSERT_TRUE(fake_device_info_); |
| device_manager_.RemoveDevice(fake_device_info_->guid); |
| fake_device_info_ = nullptr; |
| } |
| |
| const GURL& origin() { return origin_; } |
| |
| void UseFakeChooser() { |
| test_content_browser_client_.delegate().UseFakeChooser(); |
| } |
| |
| UsbChooserContext* GetChooserContext() { |
| return UsbChooserContextFactory::GetForProfile(browser()->profile()); |
| } |
| |
| private: |
| device::FakeUsbDeviceManager device_manager_; |
| device::mojom::UsbDeviceInfoPtr fake_device_info_; |
| TestUsbContentBrowserClient test_content_browser_client_; |
| GURL origin_; |
| }; |
| |
| scoped_refptr<device::FakeUsbDeviceInfo> CreateUsbDevice( |
| uint8_t class_code, |
| uint16_t product_id = 0x8765) { |
| auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New(); |
| alternate_setting->alternate_setting = 0; |
| alternate_setting->class_code = class_code; |
| |
| auto interface = device::mojom::UsbInterfaceInfo::New(); |
| interface->interface_number = 0; |
| interface->alternates.push_back(std::move(alternate_setting)); |
| |
| auto config = device::mojom::UsbConfigurationInfo::New(); |
| config->configuration_value = 1; |
| config->interfaces.push_back(std::move(interface)); |
| |
| std::vector<device::mojom::UsbConfigurationInfoPtr> configs; |
| configs.push_back(std::move(config)); |
| |
| return base::MakeRefCounted<device::FakeUsbDeviceInfo>( |
| 0x4321, product_id, "ACME", "Frobinator", "ABCDEF", std::move(configs)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, RequestAndGetDevices) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Call getDevices with no device permissions. |
| EXPECT_EQ(content::ListValueOf(), EvalJs(web_contents, |
| R"((async () => { |
| let devices = await navigator.usb.getDevices(); |
| return devices.map(device => device.serialNumber); |
| })())")); |
| |
| // Request permission to access a device. The chooser will automatically |
| // select the item representing the fake device, granting the permission. |
| UseFakeChooser(); |
| EXPECT_EQ("123456", EvalJs(web_contents, |
| R"((async () => { |
| let device = |
| await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] }); |
| return device.serialNumber; |
| })())")); |
| |
| // Call getDevices again. This time the fake device is included. |
| EXPECT_EQ(content::ListValueOf("123456"), EvalJs(web_contents, |
| R"((async () => { |
| let devices = await navigator.usb.getDevices(); |
| return devices.map(device => device.serialNumber); |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, 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); |
| |
| UseFakeChooser(); |
| EXPECT_EQ( |
| "NotFoundError: Failed to execute 'requestDevice' on 'USB': 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(ChromeWebUsbTest, AddRemoveDevice) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| UseFakeChooser(); |
| 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 }); |
| }); |
| )")); |
| |
| // The device is eligible for perisistent permissions, so the permission is |
| // remembered and a connect event is fired when the device is reconnected. |
| AddFakeDevice("123456"); |
| EXPECT_EQ("123456", content::EvalJs(web_contents, "addedPromise")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, AddRemoveDeviceEphemeral) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Replace the default mock device with one that has no serial number. |
| RemoveFakeDevice(); |
| AddFakeDevice(""); |
| |
| UseFakeChooser(); |
| 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")); |
| |
| EXPECT_TRUE(content::ExecJs(web_contents, |
| R"( |
| var sawConnectEvent = false; |
| var addedPromise = new Promise(resolve => { |
| navigator.usb.addEventListener('connect', e => { |
| sawConnectEvent = true; |
| resolve(e.device.serialNumber); |
| }, { once: true }); |
| }); |
| )")); |
| |
| // The ephemeral device permission is not persisted after disconnection, so |
| // the connect event should not be fired when the device is reconnected. |
| AddFakeDevice(""); |
| |
| // Call getDevices and wait for it to return to force synchronization before |
| // checking `sawConnectEvent`. |
| EXPECT_EQ(false, content::EvalJs(web_contents, R"((async () => { |
| await navigator.usb.getDevices(); |
| return sawConnectEvent; |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, NavigateWithChooserCrossOrigin) { |
| 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(ChromeWebUsbTest, ShowChooserInBackgroundTab) { |
| // Create a new foreground tab that covers `background_web_contents`. |
| content::WebContents* background_web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| 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: Failed to execute 'requestDevice' on 'USB': No device " |
| "selected.", |
| content::EvalJs(background_web_contents, |
| R"((async () => { |
| try { |
| await navigator.usb.requestDevice({ filters: [] }); |
| return "Expected error, got success."; |
| } catch (e) { |
| return `${e.name}: ${e.message}`; |
| } |
| })())")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, ForgetDevice) { |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| base::HistogramTester histogram_tester; |
| UseFakeChooser(); |
| EXPECT_EQ(1, content::EvalJs(web_contents, |
| R"((async () => { |
| await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] }); |
| const devices = await navigator.usb.getDevices(); |
| return devices.length; |
| })())")); |
| |
| EXPECT_EQ(0, content::EvalJs(web_contents, |
| R"((async () => { |
| const [device] = await navigator.usb.getDevices(); |
| await device.forget(); |
| const devices = await navigator.usb.getDevices(); |
| return devices.length; |
| })())")); |
| histogram_tester.ExpectUniqueSample("WebUsb.PermissionRevoked", |
| WEBUSB_PERMISSION_REVOKED_BY_WEBSITE, 1); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| class ChromeWebUsbAppTest : public extensions::ExtensionBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| ExtensionBrowserTest::SetUpOnMainThread(); |
| |
| mojo::PendingRemote<device::mojom::UsbDeviceManager> remote; |
| device_manager_.AddReceiver(remote.InitWithNewPipeAndPassReceiver()); |
| GetChooserContext()->SetDeviceManagerForTesting(std::move(remote)); |
| } |
| |
| protected: |
| UsbChooserContext* GetChooserContext() { |
| return UsbChooserContextFactory::GetForProfile(browser()->profile()); |
| } |
| |
| device::FakeUsbDeviceManager& device_manager() { return device_manager_; } |
| |
| private: |
| device::FakeUsbDeviceManager device_manager_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ChromeWebUsbAppTest, AllowProtectedInterfaces) { |
| extensions::TestExtensionDir dir; |
| dir.WriteManifest(R"( |
| { |
| "name": "WebUsbTest App", |
| "version": "1.0", |
| "manifest_version": 2, |
| "app": { |
| "background": { |
| "scripts": ["background_script.js"] |
| } |
| } |
| } |
| )"); |
| |
| dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"( |
| chrome.test.sendMessage("ready", async () => { |
| try { |
| const devices = await navigator.usb.getDevices(); |
| const device = devices[0]; |
| await device.open(); |
| await device.selectConfiguration(1); |
| await device.claimInterface(0); |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| )"); |
| |
| // Launch the test app. |
| ExtensionTestMessageListener ready_listener("ready", |
| ReplyBehavior::kWillReply); |
| extensions::ResultCatcher result_catcher; |
| scoped_refptr<const extensions::Extension> extension = |
| LoadExtension(dir.UnpackedPath()); |
| |
| // Configure the test device. |
| auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass); |
| auto device_info = device_manager().AddDevice(fake_device_info); |
| GetChooserContext()->GrantDevicePermission(extension->origin(), *device_info); |
| |
| // Run the test. |
| EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); |
| ready_listener.Reply("ok"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| class IsolatedWebAppUsbBrowserTest |
| : public web_app::IsolatedWebAppBrowserTestHarness { |
| public: |
| ~IsolatedWebAppUsbBrowserTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| IsolatedWebAppBrowserTestHarness::SetUpOnMainThread(); |
| |
| mojo::PendingRemote<device::mojom::UsbDeviceManager> remote; |
| device_manager_.AddReceiver(remote.InitWithNewPipeAndPassReceiver()); |
| chooser_context()->SetDeviceManagerForTesting(std::move(remote)); |
| } |
| |
| protected: |
| UsbChooserContext* chooser_context() { |
| return UsbChooserContextFactory::GetForProfile(profile()); |
| } |
| device::FakeUsbDeviceManager& device_manager() { return device_manager_; } |
| |
| private: |
| device::FakeUsbDeviceManager device_manager_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppUsbBrowserTest, ClaimInterface) { |
| // Verifies that non-IWA main frames and cross-origin iframes in an IWA can |
| // access normal USB devices, but not devices from a protected class. IWA |
| // frames without usb-unrestricted permission can only access non-protected |
| // class too. |
| GURL frame_url = https_server()->GetURL("/banners/isolated/simple.html"); |
| auto* non_app_main_frame = ui_test_utils::NavigateToURL(browser(), frame_url); |
| |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| web_app::CreateIframe(app_frame, "child", frame_url, |
| /*permissions_policy=*/"usb *"); |
| auto* delegated_non_app_iframe = ChildFrameAt(app_frame, 0); |
| |
| const uint16_t kSmartCardProductId = 0x8765; |
| auto fake_smart_card_device_info = |
| CreateUsbDevice(device::mojom::kUsbSmartCardClass, kSmartCardProductId); |
| auto smart_card_device_info = |
| device_manager().AddDevice(std::move(fake_smart_card_device_info)); |
| chooser_context()->GrantDevicePermission( |
| non_app_main_frame->GetLastCommittedOrigin(), *smart_card_device_info); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *smart_card_device_info); |
| |
| const uint16_t kPrinterProductId = 0x5678; |
| auto fake_printer_device_info = |
| CreateUsbDevice(kUsbPrinterClass, kPrinterProductId); |
| auto printer_device_info = |
| device_manager().AddDevice(std::move(fake_printer_device_info)); |
| chooser_context()->GrantDevicePermission( |
| non_app_main_frame->GetLastCommittedOrigin(), *printer_device_info); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *printer_device_info); |
| |
| constexpr char kClaimInterface[] = R"((async () => { |
| const devices = await navigator.usb.getDevices(); |
| const device = devices.filter((device) => device.productId === $1)[0]; |
| await device.open(); |
| await device.selectConfiguration(1); |
| await device.claimInterface(0); |
| return "Success"; |
| })())"; |
| |
| EXPECT_EQ("Success", |
| EvalJs(app_frame, JsReplace(kClaimInterface, kPrinterProductId))); |
| EXPECT_THAT( |
| EvalJs(app_frame, JsReplace(kClaimInterface, kSmartCardProductId)), |
| FailedWithSubstr("requested interface implements a protected class")); |
| |
| EXPECT_EQ("Success", EvalJs(non_app_main_frame, |
| JsReplace(kClaimInterface, kPrinterProductId))); |
| EXPECT_THAT( |
| EvalJs(non_app_main_frame, |
| JsReplace(kClaimInterface, kSmartCardProductId)), |
| FailedWithSubstr("requested interface implements a protected class")); |
| |
| EXPECT_EQ("Success", EvalJs(delegated_non_app_iframe, |
| JsReplace(kClaimInterface, kPrinterProductId))); |
| EXPECT_THAT( |
| EvalJs(delegated_non_app_iframe, |
| JsReplace(kClaimInterface, kSmartCardProductId)), |
| FailedWithSubstr("requested interface implements a protected class")); |
| } |
| |
| class IsolatedWebAppPermissionsPolicyBrowserTest |
| : public IsolatedWebAppUsbBrowserTest { |
| public: |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| IsolatedWebAppUsbBrowserTest::SetUpCommandLine(command_line); |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Iframe_NoAllowAttribute) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Create a same-origin iframe on the page that does not specify an |
| // allow attribute, and expect that usb is accessible on the main frame, as |
| // well as in the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const std::string permissions_policy = ""; |
| web_app::CreateIframe(app_frame, "child", GURL("/"), permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| |
| // Create a cross-origin iframe and expect usb to be disabled in that context |
| // since it does not specify usb in the allowlist. |
| GURL non_app_url = |
| https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html"); |
| web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy); |
| iframe = ChildFrameAt(app_frame, 1); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Iframe_Self) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Create a same-origin iframe on the page that specifies an allow |
| // attribute allowing usb for 'self', and expect that usb is accessible on the |
| // main frame, as well as in the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const std::string permissions_policy = "usb 'self'"; |
| web_app::CreateIframe(app_frame, "child", GURL("/"), permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| |
| // Perform a cross-origin navigation in the iframe, which should no longer |
| // match the 'self' permissions policy token, and verify the permissions |
| // policy blocks access to usb. |
| GURL non_app_url = |
| https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html"); |
| EXPECT_TRUE(content::NavigateToURLFromRenderer(iframe, non_app_url)); |
| iframe = ChildFrameAt(app_frame, 0); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| // TODO(crbug.com/398292721): Flaky on Mac and Linux builds. |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) |
| #define MAYBE_PermissionsPolicy_Iframe_Src DISABLED_PermissionsPolicy_Iframe_Src |
| #else |
| #define MAYBE_PermissionsPolicy_Iframe_Src PermissionsPolicy_Iframe_Src |
| #endif |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| MAYBE_PermissionsPolicy_Iframe_Src) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Create a cross-origin iframe on the page that specifies an allow |
| // attribute allowing usb for 'src', and expect that usb is accessible on the |
| // main frame, as well as in the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL non_app_url = |
| https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html"); |
| const std::string permissions_policy = "usb 'src'"; |
| web_app::CreateIframe(app_frame, "child", non_app_url, permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| |
| // Perform a navigation in the iframe to a cross-origin resource (a distinct |
| // origin from that which the iframe originally loaded, as well as the main |
| // frame), which should no longer match the 'src' permissions policy token, |
| // and verify the permissions policy blocks access to usb. |
| GURL non_app_url_2 = |
| https_server()->GetURL(kNonAppHost2, "/banners/isolated/simple.html"); |
| EXPECT_TRUE(content::NavigateToURLFromRenderer(iframe, non_app_url_2)); |
| iframe = ChildFrameAt(app_frame, 0); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Iframe_None) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Create a cross-origin iframe on the page that specifies an allow |
| // attribute allowing usb with the 'none' token, and expect that usb is |
| // accessible on the main frame, but is blocked by permissions policy in the |
| // iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| const std::string permissions_policy = "usb 'none'"; |
| web_app::CreateIframe(app_frame, "child", GURL("/index.html"), |
| permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Iframe_CrossOrigin) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Create a cross-origin iframe on the page that specifies an allow |
| // attribute allowing usb for the iframe by explicitly listing the iframe |
| // origin in the allowlist, and expect that usb is accessible on the main |
| // frame as well as in the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL non_app_url = |
| https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html"); |
| const std::string permissions_policy = base::StringPrintf( |
| "usb %s", https_server()->GetURL(kNonAppHost, "/").spec().c_str()); |
| web_app::CreateIframe(app_frame, "child", non_app_url, permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Headers_None) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Load a page in the app window that has a Permissions Policy |
| // header which does not allow usb on any origin (using an empty allowlist). |
| // Create a same-origin iframe on the page that does not specify an allow |
| // attribute, and expect that usb is not accessible on the main frame or in |
| // the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .AddFileFromDisk("/usb_none.html", |
| "web_apps/simple_isolated_app/usb_none.html") |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/usb_none.html"); |
| auto* app_browser = GetBrowserFromFrame(app_frame); |
| app_frame = ui_test_utils::NavigateToURL(app_browser, app_url); |
| |
| const std::string permissions_policy = ""; |
| web_app::CreateIframe(app_frame, "child", app_url, permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_THAT(EvalJs(app_frame, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| |
| // Create a cross-origin iframe and expect usb to be disabled in that context. |
| GURL non_app_url = https_server()->GetURL( |
| kNonAppHost, "/web_apps/simple_isolated_app/usb_none.html"); |
| web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy); |
| iframe = ChildFrameAt(app_frame, 1); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Headers_Self) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Load a page in the app window that has a Permissions Policy |
| // header which allows usb on the same origin using the 'self' token. Create a |
| // same-origin iframe on the page that does not specify an allow attribute, |
| // and expect that usb is accessible on the main frame, as well as in the |
| // iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .AddFileFromDisk("/usb_self.html", |
| "web_apps/simple_isolated_app/usb_self.html") |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/usb_self.html"); |
| auto* app_browser = GetBrowserFromFrame(app_frame); |
| app_frame = ui_test_utils::NavigateToURL(app_browser, app_url); |
| |
| const std::string permissions_policy = ""; |
| web_app::CreateIframe(app_frame, "child", app_url, permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| |
| // Create a cross-origin iframe and expect usb to be disabled in that context. |
| GURL non_app_url = https_server()->GetURL( |
| kNonAppHost, "/web_apps/simple_isolated_app/usb_self.html"); |
| web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy); |
| iframe = ChildFrameAt(app_frame, 1); |
| |
| EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Headers_All) { |
| // Install an Isolated Web App that has usb turned on for all origins in its |
| // manifest. Load a page in the app window that has a Permissions Policy |
| // header which allows usb on any origin. Create a same-origin iframe on the |
| // page that does not specify an allow attribute, and expect that usb is |
| // accessible on the main frame, as well as in the iframe. |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder().AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb)) |
| .AddFileFromDisk("/usb_all.html", |
| "web_apps/simple_isolated_app/usb_all.html") |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| GURL app_url = url_info.origin().GetURL().Resolve("/usb_all.html"); |
| auto* app_browser = GetBrowserFromFrame(app_frame); |
| app_frame = ui_test_utils::NavigateToURL(app_browser, app_url); |
| |
| const std::string permissions_policy = ""; |
| web_app::CreateIframe(app_frame, "child", app_url, permissions_policy); |
| auto* iframe = ChildFrameAt(app_frame, 0); |
| |
| auto fake_device_info = CreateUsbDevice(kUsbPrinterClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| |
| // Create a cross-origin iframe with "usb" in the allow attribute and expect |
| // usb to be enabled in that context. |
| GURL non_app_url = https_server()->GetURL( |
| kNonAppHost, "/web_apps/simple_isolated_app/usb_all.html"); |
| web_app::CreateIframe(app_frame, "child2", non_app_url, "usb"); |
| iframe = ChildFrameAt(app_frame, 1); |
| |
| EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Usb_Unrestricted_CrossOrigin_Iframe) { |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder() |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb) |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsbUnrestricted) |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature:: |
| kCrossOriginIsolated)) |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| // Create a fake device with protected class and grant permission. |
| auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| |
| // With "usb-unrestricted" permission, when main frame claims protected class |
| // device it should succeed. |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| |
| GURL cross_origin_iframe_url = |
| https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html"); |
| |
| // Create a cross-origin Iframe without any permission and request to |
| // protected class device should be denied due to "usb" feature is not |
| // enabled on iframe's document. |
| web_app::CreateIframe(app_frame, "child0", cross_origin_iframe_url, ""); |
| auto* cross_origin_iframe0 = ChildFrameAt(app_frame, 0); |
| EXPECT_THAT( |
| EvalJs(cross_origin_iframe0, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| |
| // Create a cross-origin Iframe with only "usb-unrestricted" permission, |
| // request to protected class device should be denied due to "usb" feature is |
| // not enabled on iframe's document. |
| web_app::CreateIframe(app_frame, "child1", cross_origin_iframe_url, |
| "usb-unrestricted"); |
| auto* cross_origin_iframe1 = ChildFrameAt(app_frame, 1); |
| EXPECT_THAT( |
| EvalJs(cross_origin_iframe1, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("permissions policy.")); |
| |
| // Create a cross-origin Iframe with only "usb" permission, request to |
| // protected class device should be denied due to "usb-unrestricted" is not |
| // enabled. |
| web_app::CreateIframe(app_frame, "child2", cross_origin_iframe_url, "usb"); |
| auto* cross_origin_iframe2 = ChildFrameAt(app_frame, 2); |
| EXPECT_THAT( |
| EvalJs(cross_origin_iframe2, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("requested interface implements a protected class.")); |
| |
| // Create a cross-origin Iframe with "usb + usb-unrestricted" and request to |
| // protected class device should be denied due to iframe's isolation level = |
| // 0. |
| web_app::CreateIframe(app_frame, "child3", cross_origin_iframe_url, |
| "usb; usb-unrestricted"); |
| auto* cross_origin_iframe3 = ChildFrameAt(app_frame, 3); |
| EXPECT_THAT( |
| EvalJs(cross_origin_iframe3, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("requested interface implements a protected class.")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest, |
| PermissionsPolicy_Usb_Unrestricted_Iframe) { |
| std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app = |
| web_app::IsolatedWebAppBuilder( |
| web_app::ManifestBuilder() |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsb) |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature::kUsbUnrestricted) |
| .AddPermissionsPolicyWildcard( |
| network::mojom::PermissionsPolicyFeature:: |
| kCrossOriginIsolated)) |
| .AddHtml("/empty.html", "Empty Page") |
| .BuildBundle(); |
| ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info, |
| app->Install(profile())); |
| content::RenderFrameHost* app_frame = OpenApp(url_info.app_id()); |
| |
| // Create a fake device with protected class and grant permission. |
| auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass); |
| auto device_info = device_manager().AddDevice(std::move(fake_device_info)); |
| chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(), |
| *device_info); |
| |
| // With "usb + usb-unrestricted" permission, when main frame claims protected |
| // class device it should succeed. |
| EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript)); |
| |
| // Create a same-origin iframe without any permissions in attribute, request |
| // to protected class device should still succeed due to "usb + |
| // usb-unrestricted" feature is inherited from main frame and same-origin |
| // iframe is still isolated. |
| web_app::CreateIframe(app_frame, "child0", GURL("empty.html"), ""); |
| auto* iframe0 = ChildFrameAt(app_frame, 0); |
| EXPECT_EQ("Success", EvalJs(iframe0, OpenAndClaimDeviceScript)); |
| |
| // Create a same-origin iframe with "usb-unrestricted" permissions disabled, |
| // request to protected class device should fail. |
| web_app::CreateIframe(app_frame, "child1", GURL("empty.html"), |
| "usb-unrestricted 'none'"); |
| auto* iframe1 = ChildFrameAt(app_frame, 1); |
| EXPECT_THAT( |
| EvalJs(iframe1, OpenAndClaimDeviceScript).ExtractString(), |
| testing::EndsWith("requested interface implements a protected class.")); |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| class WebUsbExtensionBrowserTest : public InProcessBrowserTestMixinHostSupport< |
| extensions::ExtensionBrowserTest> { |
| public: |
| WebUsbExtensionBrowserTest() { |
| #if BUILDFLAG(IS_CHROMEOS) |
| // The user is created via RegularLoggedInBrowserTestMixin. |
| set_chromeos_user_ = false; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| void SetUpOnMainThread() override { |
| InProcessBrowserTestMixinHostSupport< |
| extensions::ExtensionBrowserTest>::SetUpOnMainThread(); |
| #if BUILDFLAG(IS_CHROMEOS) |
| user_manager::UserManager::Get()->SetUserPolicyStatus( |
| kManagedUserAccountId, /*is_managed=*/true, /*is_affiliated=*/true); |
| display_service_for_system_notification_ = |
| std::make_unique<NotificationDisplayServiceTester>( |
| /*profile=*/nullptr); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager; |
| device_manager_.AddReceiver( |
| device_manager.InitWithNewPipeAndPassReceiver()); |
| |
| // Connect the UsbDeviceManager and ensure we've received the initial |
| // enumeration before continuing. |
| auto* chooser_context = UsbChooserContextFactory::GetForProfile(profile()); |
| chooser_context->SetDeviceManagerForTesting(std::move(device_manager)); |
| TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> devices_future; |
| chooser_context->GetDevices(devices_future.GetCallback()); |
| ASSERT_TRUE(devices_future.Get().empty()); |
| } |
| |
| void SetUpPolicy(const extensions::Extension* extension) { |
| // Define a policy to automatically grant permission to access the device |
| // created by AddFakeDevice. |
| constexpr char kPolicyTemplate[] = R"( |
| [ |
| { |
| "devices": [{ "vendor_id": 1234, "product_id": 5678 }], |
| "urls": ["%s"] |
| } |
| ])"; |
| profile()->GetPrefs()->Set( |
| prefs::kManagedWebUsbAllowDevicesForUrls, |
| base::test::ParseJson(base::StringPrintf( |
| kPolicyTemplate, extension->url().spec().c_str()))); |
| } |
| |
| void SetUpTestDir(extensions::TestExtensionDir& test_dir, |
| std::string_view background_js) { |
| test_dir.WriteManifest(base::StringPrintf( |
| R"({ |
| "name": "Test Extension", |
| "version": "0.1", |
| "key": "%s", |
| "manifest_version": 3, |
| "background": { |
| "service_worker": "background.js" |
| } |
| })", |
| kTestExtensionKey)); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), background_js); |
| } |
| |
| const Extension* LoadExtensionAndRunTest(std::string_view background_js) { |
| extensions::TestExtensionDir test_dir; |
| SetUpTestDir(test_dir, background_js); |
| |
| // Launch the test app. |
| ExtensionTestMessageListener ready_listener("ready", |
| ReplyBehavior::kWillReply); |
| extensions::ResultCatcher result_catcher; |
| const extensions::Extension* extension = |
| LoadExtension(test_dir.UnpackedPath()); |
| CHECK(extension); |
| CHECK_EQ(extension->id(), kTestExtensionId); |
| |
| // TODO(crbug.com/40847683): Grant permission using requestDevice(). |
| // Run the test. |
| SetUpPolicy(extension); |
| EXPECT_TRUE(ready_listener.WaitUntilSatisfied()); |
| ready_listener.Reply("ok"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| |
| return extension; |
| } |
| |
| device::FakeUsbDeviceManager& device_manager() { return device_manager_; } |
| |
| void AddFakeDevice() { |
| DCHECK(!fake_device_info_); |
| fake_device_info_ = device_manager_.CreateAndAddDevice( |
| 1234, 5678, "Test Manufacturer", "Test Device", "123456"); |
| } |
| |
| void RemoveFakeDevice() { |
| DCHECK(fake_device_info_); |
| device_manager_.RemoveDevice(fake_device_info_->guid); |
| fake_device_info_ = nullptr; |
| } |
| |
| void SimulateClickOnSystemTrayIconButton(Browser* browser, |
| const Extension* extension) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| auto* usb_pinned_notification = static_cast<UsbPinnedNotification*>( |
| g_browser_process->usb_system_tray_icon()); |
| |
| auto* device_pinned_notification_renderer = |
| static_cast<DevicePinnedNotificationRenderer*>( |
| usb_pinned_notification->GetIconRendererForTesting()); |
| |
| auto expected_pinned_notification_id = |
| device_pinned_notification_renderer->GetNotificationId( |
| browser->profile()); |
| auto maybe_indicator_notification = |
| display_service_for_system_notification_->GetNotification( |
| expected_pinned_notification_id); |
| ASSERT_TRUE(maybe_indicator_notification); |
| EXPECT_TRUE(maybe_indicator_notification->pinned()); |
| display_service_for_system_notification_->SimulateClick( |
| NotificationHandler::Type::TRANSIENT, expected_pinned_notification_id, |
| /*action_index=*/0, /*reply=*/std::nullopt); |
| auto* web_contents = browser->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_EQ(web_contents->GetURL(), "chrome://settings/content/usbDevices"); |
| #else |
| // On non-ChromeOS platforms, as they use status icon and there isn't good |
| // test infra to simulate click on the status icon button, so simulate the |
| // click event by invoking ExecuteCommand of UsbConnectionTracker directly. |
| auto* usb_status_icon = |
| static_cast<UsbStatusIcon*>(g_browser_process->usb_system_tray_icon()); |
| |
| auto* status_icon_renderer = static_cast<DeviceStatusIconRenderer*>( |
| usb_status_icon->GetIconRendererForTesting()); |
| |
| status_icon_renderer->ExecuteCommandForTesting( |
| IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST, 0); |
| EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents()->GetURL(), |
| "https://support.google.com/chrome?p=webusb"); |
| |
| status_icon_renderer->ExecuteCommandForTesting( |
| IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST + 1, 0); |
| EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents()->GetURL(), |
| "chrome://settings/content/usbDevices"); |
| |
| status_icon_renderer->ExecuteCommandForTesting( |
| IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST + 2, 0); |
| EXPECT_EQ( |
| browser->tab_strip_model()->GetActiveWebContents()->GetURL(), |
| "chrome://settings/content/siteDetails?site=chrome-extension%3A%2F%2F" + |
| extension->id()); |
| #endif |
| } |
| |
| private: |
| device::FakeUsbDeviceManager device_manager_; |
| device::mojom::UsbDeviceInfoPtr fake_device_info_; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| ash::RegularLoggedInBrowserTestMixin logged_in_mixin_{&mixin_host_, |
| kManagedUserAccountId}; |
| std::unique_ptr<NotificationDisplayServiceTester> |
| display_service_for_system_notification_; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| }; |
| |
| // TODO(crbug.com/41494522): Flaky on non-Mac release builds. |
| #if !BUILDFLAG(IS_MAC) && defined(NDEBUG) |
| #define MAYBE_GetDevices DISABLED_GetDevices |
| #else |
| #define MAYBE_GetDevices GetDevices |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_GetDevices) { |
| constexpr std::string_view kBackgroundJs = R"( |
| chrome.test.sendMessage("ready", async () => { |
| try { |
| const devices = await navigator.usb.getDevices(); |
| chrome.test.assertEq(1, devices.length); |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| )"; |
| AddFakeDevice(); |
| LoadExtensionAndRunTest(kBackgroundJs); |
| } |
| |
| // TODO(crbug.com/41494522): Flaky on non-Mac release builds. |
| #if !BUILDFLAG(IS_MAC) && defined(NDEBUG) |
| #define MAYBE_RequestDevice DISABLED_RequestDevice |
| #else |
| #define MAYBE_RequestDevice RequestDevice |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_RequestDevice) { |
| constexpr std::string_view kBackgroundJs = R"( |
| chrome.test.sendMessage("ready", async () => { |
| try { |
| chrome.test.assertEq(navigator.usb.requestDevice, undefined); |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| )"; |
| LoadExtensionAndRunTest(kBackgroundJs); |
| } |
| |
| // TODO(crbug.com/41494522): Flaky on non-Mac release builds. |
| #if !BUILDFLAG(IS_MAC) && defined(NDEBUG) |
| #define MAYBE_UsbConnectionTracker DISABLED_UsbConnectionTracker |
| #else |
| #define MAYBE_UsbConnectionTracker UsbConnectionTracker |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_UsbConnectionTracker) { |
| constexpr char kBackgroundJs[] = R"( |
| // |device| is a global variable to store UsbDevice object being tested in |
| // case the local one is garbage collected, which can close the connection. |
| var device; |
| chrome.test.sendMessage("ready", async () => { |
| try { |
| const devices = await navigator.usb.getDevices(); |
| device = devices[0]; |
| chrome.test.assertEq(1, devices.length); |
| // Bounce device a few times to make sure nothing unexpected happens. |
| await device.open(); |
| await device.close(); |
| await device.open(); |
| await device.close(); |
| await device.open(); |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| )"; |
| AddFakeDevice(); |
| const auto* extension = LoadExtensionAndRunTest(kBackgroundJs); |
| SimulateClickOnSystemTrayIconButton(browser(), extension); |
| } |
| |
| // Test the scenario of waking up the service worker upon device events and |
| // the service worker being kept alive with active device session. |
| // TODO(crbug.com/41494522): Flaky on non-Mac release builds and ChromeOS |
| // builds. |
| #if (!BUILDFLAG(IS_MAC) && defined(NDEBUG)) || BUILDFLAG(IS_CHROMEOS) |
| #define MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped \ |
| DISABLED_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped |
| #else |
| #define MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped \ |
| DeviceConnectAndOpenDeviceWhenServiceWorkerStopped |
| #endif |
| IN_PROC_BROWSER_TEST_F( |
| WebUsbExtensionBrowserTest, |
| MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped) { |
| content::ServiceWorkerContext* context = browser() |
| ->profile() |
| ->GetDefaultStoragePartition() |
| ->GetServiceWorkerContext(); |
| // Set up an observer for service worker events. |
| TestServiceWorkerContextObserver sw_observer(context, kTestExtensionId); |
| |
| TestExtensionDir test_dir; |
| constexpr char kBackgroundJs[] = R"( |
| navigator.usb.onconnect = async (e) => { |
| chrome.test.sendMessage("connect", async () => { |
| try { |
| let device = e.device; |
| // Bounce device a few times to make sure nothing unexpected |
| // happens. |
| await device.open(); |
| await device.close(); |
| await device.open(); |
| await device.close(); |
| await device.open(); |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| } |
| |
| navigator.usb.ondisconnect = async (e) => { |
| chrome.test.sendMessage("disconnect", async () => { |
| try { |
| chrome.test.notifyPass(); |
| } catch (e) { |
| chrome.test.fail(e.name + ':' + e.message); |
| } |
| }); |
| } |
| )"; |
| SetUpTestDir(test_dir, kBackgroundJs); |
| |
| // Launch the test app. |
| ExtensionTestMessageListener connect_listener("connect", |
| ReplyBehavior::kWillReply); |
| extensions::ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| // TODO(crbug.com/40847683): Grant permission using requestDevice(). |
| // Run the test. |
| SetUpPolicy(extension); |
| ASSERT_TRUE(extension); |
| ASSERT_EQ(extension->id(), kTestExtensionId); |
| sw_observer.WaitForWorkerStart(); |
| sw_observer.WaitForWorkerActivated(); |
| |
| // The device event is handled right after the service worker is activated. |
| int64_t service_worker_version_id = sw_observer.GetServiceWorkerVersionId(); |
| base::SimpleTestTickClock tick_clock; |
| AddFakeDevice(); |
| EXPECT_TRUE(connect_listener.WaitUntilSatisfied()); |
| connect_listener.Reply("ok"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| // Advance clock and the service worker is still alive due to active device |
| // session. |
| content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id, |
| &tick_clock); |
| EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState( |
| context, service_worker_version_id)); |
| // Since we have active USB device session at this point, click the USB system |
| // tray icon and check right links are opened by the browser. |
| SimulateClickOnSystemTrayIconButton(browser(), extension); |
| |
| // Remove device will close the device session, and worker will stop running |
| // when it times out. |
| ExtensionTestMessageListener disconnect_listener("disconnect", |
| ReplyBehavior::kWillReply); |
| RemoveFakeDevice(); |
| EXPECT_TRUE(disconnect_listener.WaitUntilSatisfied()); |
| disconnect_listener.Reply("ok"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| // Advance clock and check that the receiver service worker stopped. |
| content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id, |
| &tick_clock); |
| EXPECT_FALSE(content::TriggerTimeoutAndCheckRunningState( |
| context, service_worker_version_id)); |
| sw_observer.WaitForWorkerStop(); |
| |
| // Another device event wakes up the inactive worker. |
| connect_listener.Reset(); |
| AddFakeDevice(); |
| EXPECT_TRUE(connect_listener.WaitUntilSatisfied()); |
| connect_listener.Reply("ok"); |
| EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message(); |
| // Advance clock and the service worker is still alive due to active device |
| // session. |
| content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id, |
| &tick_clock); |
| EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState( |
| context, service_worker_version_id)); |
| // Since we have active USB device session at this point, click the USB system |
| // tray icon and check right links are opened by the browser. |
| SimulateClickOnSystemTrayIconButton(browser(), extension); |
| } |
| |
| // TODO(crbug.com/41494522): Flaky on non-Mac release builds. |
| #if !BUILDFLAG(IS_MAC) && defined(NDEBUG) |
| #define MAYBE_EventListenerAddedAfterServiceWorkerIsActivated \ |
| DISABLED_EventListenerAddedAfterServiceWorkerIsActivated |
| #else |
| #define MAYBE_EventListenerAddedAfterServiceWorkerIsActivated \ |
| EventListenerAddedAfterServiceWorkerIsActivated |
| #endif |
| IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, |
| MAYBE_EventListenerAddedAfterServiceWorkerIsActivated) { |
| const char kWarningMessage[] = |
| "Event handler of '%s' event must be added on the initial evaluation " |
| "of worker script. More info: " |
| "https://developer.chrome.com/docs/extensions/mv3/service_workers/" |
| "events/"; |
| |
| content::ServiceWorkerContext* context = browser() |
| ->profile() |
| ->GetDefaultStoragePartition() |
| ->GetServiceWorkerContext(); |
| // Set up an observer for service worker events. |
| TestServiceWorkerContextObserver sw_observer(context, kTestExtensionId); |
| // Set up an observer for console messages reported by service worker |
| TestServiceWorkerConsoleObserver console_observer(browser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetBrowserContext()); |
| TestExtensionDir test_dir; |
| constexpr char kBackgroundJs[] = R"( |
| chrome.test.sendMessage("ready", function() { |
| navigator.usb.addEventListener("connect", () => {}); |
| }); |
| )"; |
| SetUpTestDir(test_dir, kBackgroundJs); |
| |
| // Launch the test app. |
| extensions::ResultCatcher result_catcher; |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| // TODO(crbug.com/40847683): Grant permission using requestDevice(). |
| // Run the test. |
| SetUpPolicy(extension); |
| ASSERT_TRUE(extension); |
| ASSERT_EQ(extension->id(), kTestExtensionId); |
| sw_observer.WaitForWorkerStart(); |
| sw_observer.WaitForWorkerActivated(); |
| AddFakeDevice(); |
| |
| // Warning message will be displayed when event listener is nested inside a |
| // function |
| console_observer.WaitForMessages(); |
| EXPECT_EQ(console_observer.messages().size(), 1u); |
| EXPECT_EQ(console_observer.messages().begin()->message_level, |
| blink::mojom::ConsoleMessageLevel::kWarning); |
| EXPECT_EQ(console_observer.messages().begin()->message, |
| base::UTF8ToUTF16(base::StringPrintf(kWarningMessage, "connect"))); |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| } // namespace |