blob: e61f7bd37d38842f3a7c8246d9a8cd4350ff0edc [file] [log] [blame]
// Copyright 2025 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 "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "chrome/browser/serial/chrome_serial_delegate.h"
#include "chrome/browser/serial/web_serial_chooser.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/serial/serial_chooser_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_socket.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
namespace {
constexpr int kMicroBitVendorId = 0x0d28;
constexpr int kMicroBitProductId = 0x0204;
constexpr std::string_view kPixelBudsProSerialPortUuid =
"25e97ff7-24ce-4c4c-8951-f764a708f7b5";
// The chooser view that will select the first device in the list upon the list
// initialization or the first device added.
class SerialRealTargetTestChooserView
: public permissions::ChooserController::View {
public:
explicit SerialRealTargetTestChooserView(
std::unique_ptr<permissions::ChooserController> controller)
: controller_(std::move(controller)) {
controller_->set_view(this);
}
SerialRealTargetTestChooserView(const SerialRealTargetTestChooserView&) =
delete;
SerialRealTargetTestChooserView& operator=(
const SerialRealTargetTestChooserView&) = delete;
~SerialRealTargetTestChooserView() override {
controller_->set_view(nullptr);
}
void OnOptionsInitialized() override {
if (!selected_ && controller_->NumOptions() > 0) {
controller_->Select({0});
selected_ = true;
}
}
void OnOptionAdded(size_t index) override {
if (!selected_ && controller_->NumOptions() > 0) {
controller_->Select({0});
selected_ = true;
}
}
void OnOptionRemoved(size_t index) override {}
void OnOptionUpdated(size_t index) override {}
void OnAdapterEnabledChanged(bool enabled) override {}
void OnRefreshStateChanged(bool refreshing) override {}
private:
std::unique_ptr<permissions::ChooserController> controller_;
bool selected_ = false;
};
class SerialRealTargetTestChooser : public WebSerialChooser {
public:
SerialRealTargetTestChooser() = default;
SerialRealTargetTestChooser(const SerialRealTargetTestChooser&) = delete;
SerialRealTargetTestChooser& operator=(const SerialRealTargetTestChooser&) =
delete;
~SerialRealTargetTestChooser() override = default;
void ShowChooser(
content::RenderFrameHost* frame,
std::unique_ptr<SerialChooserController> controller) override {
view_ = std::make_unique<SerialRealTargetTestChooserView>(
std::move(controller));
}
private:
std::unique_ptr<SerialRealTargetTestChooserView> view_;
};
class SerialRealTargetTestDelegate : public ChromeSerialDelegate {
public:
SerialRealTargetTestDelegate() = default;
~SerialRealTargetTestDelegate() override = default;
std::unique_ptr<content::SerialChooser> RunChooser(
content::RenderFrameHost* frame,
std::vector<blink::mojom::SerialPortFilterPtr> filters,
std::vector<device::BluetoothUUID> allowed_bluetooth_service_class_ids,
content::SerialChooser::Callback callback) override {
auto chooser = std::make_unique<SerialRealTargetTestChooser>();
chooser->ShowChooser(frame,
std::make_unique<SerialChooserController>(
frame, std::move(filters),
std::move(allowed_bluetooth_service_class_ids),
std::move(callback)));
return chooser;
}
};
class TestContentBrowserClient : public content::ContentBrowserClient {
public:
TestContentBrowserClient()
: serial_delegate_(std::make_unique<SerialRealTargetTestDelegate>()) {}
~TestContentBrowserClient() override = default;
content::SerialDelegate* GetSerialDelegate() override {
return serial_delegate_.get();
}
void SetAsBrowserClient() {
original_content_browser_client_ =
content::SetBrowserClientForTesting(this);
}
void UnsetAsBrowserClient() {
content::SetBrowserClientForTesting(original_content_browser_client_);
serial_delegate_.reset();
}
private:
std::unique_ptr<SerialRealTargetTestDelegate> serial_delegate_;
raw_ptr<content::ContentBrowserClient> original_content_browser_client_ =
nullptr;
};
class SerialRealTargetTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
test_content_browser_client_.SetAsBrowserClient();
GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html");
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
content::RenderFrameHost* render_frame_host = browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
EXPECT_EQ(url.DeprecatedGetOriginAsURL(),
render_frame_host->GetLastCommittedOrigin().GetURL());
}
void TearDownOnMainThread() override {
test_content_browser_client_.UnsetAsBrowserClient();
}
// Attempt socket connection with `uuid_string` to all bluetooth devices known
// to the adapter.
void AttemptSocketConnectionToBluetoothDevices(std::string uuid_string) {
base::test::TestFuture<scoped_refptr<device::BluetoothAdapter>>
adapter_future;
device::BluetoothAdapterFactory::Get()->GetAdapter(
adapter_future.GetCallback());
scoped_refptr<device::BluetoothAdapter> adapter = adapter_future.Get();
ASSERT_TRUE(adapter);
for (auto* device : adapter->GetDevices()) {
base::test::TestFuture<scoped_refptr<device::BluetoothSocket>>
socket_future;
auto split_callback =
base::SplitOnceCallback(socket_future.GetCallback());
LOG(INFO) << "Attempt socket connection with " << uuid_string
<< " to device " << device->GetNameForDisplay();
// The purpose of this function is to make the system to talk to bluetooth
// devices so UUIDs of the devices will be known to the system, which
// avoid issue like empty UUIDs in BluetoothDevice when testing after
// system reboot.
device->ConnectToService(
device::BluetoothUUID(uuid_string), std::move(split_callback.first),
base::BindLambdaForTesting([&](const std::string& message) {
std::move(split_callback.second).Run(nullptr);
}));
scoped_refptr<device::BluetoothSocket> socket = socket_future.Get();
if (socket) {
base::test::TestFuture<void> disconnect_future;
socket->Disconnect(disconnect_future.GetCallback());
ASSERT_TRUE(disconnect_future.Wait());
}
}
}
private:
TestContentBrowserClient test_content_browser_client_;
};
IN_PROC_BROWSER_TEST_F(SerialRealTargetTest, SerialOpenAndClosePort) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto test_script = base::StringPrintf(
R"((async () => {
let port = await navigator.serial.requestPort(
{filters: [{ usbVendorId: %d, usbProductId: %d }]}
);
await port.open({ baudRate: 115200 });
let result = port.connected;
await port.close();
return result;
})())",
kMicroBitVendorId, kMicroBitProductId);
EXPECT_EQ(true, EvalJs(web_contents, test_script));
}
IN_PROC_BROWSER_TEST_F(SerialRealTargetTest, SetSignals) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto test_script = base::StringPrintf(
R"((async () => {
const port = await navigator.serial.requestPort(
{filters: [{ usbVendorId: %d, usbProductId: %d }]}
);
await port.open({ baudRate: 115200 });
for (let i = 3; i >= 0; --i) {
const expectedDataTerminalReady = !!(i & 0x1);
const expectedRequestToSend = !!(i & 0x2);
await port.setSignals({
dataTerminalReady: expectedDataTerminalReady,
requestToSend: expectedRequestToSend,
});
}
await port.close();
return true;
})())",
kMicroBitVendorId, kMicroBitProductId);
EXPECT_EQ(true, EvalJs(web_contents, test_script));
}
IN_PROC_BROWSER_TEST_F(SerialRealTargetTest, BluetoothSerialOpenAndClosePort) {
// This step is to make the target bluetooth device connected to the system,
// to reduce flaky when running Serial Bluetooth test.
AttemptSocketConnectionToBluetoothDevices(
std::string(kPixelBudsProSerialPortUuid));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto test_script = base::StringPrintf(
R"((async () => {
const uuids=['%s'];
let port = await navigator.serial.requestPort({
allowedBluetoothServiceClassIds:uuids, filters:[
{bluetoothServiceClassId:uuids[0]}
]
});
// The retry here is because the pixel bud in the lab is in the case
// with lid opened, where the first open attempt would fail.
async function openPortWithRetry(port, maxRetries = 2) {
for (let retry = 0; retry < maxRetries; retry++) {
try {
await port.open({ baudRate: 115200 });
return;
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
throw new Error('Failed to open port after multiple attempts.');
}
await openPortWithRetry(port);
let result = port.connected;
await port.close();
return result;
})())",
kPixelBudsProSerialPortUuid);
EXPECT_EQ(true, EvalJs(web_contents, test_script));
}
} // namespace