summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--device/hid/hid_service.cc9
-rw-r--r--device/hid/hid_service.h2
-rw-r--r--extensions/browser/api/hid/hid_apitest.cc187
-rw-r--r--extensions/shell/app_shell.gyp1
-rw-r--r--extensions/test/data/api_test/hid/api/background.js363
-rw-r--r--extensions/test/data/api_test/hid/api/manifest.json15
6 files changed, 576 insertions, 1 deletions
diff --git a/device/hid/hid_service.cc b/device/hid/hid_service.cc
index d2ccaa3..071b396 100644
--- a/device/hid/hid_service.cc
+++ b/device/hid/hid_service.cc
@@ -55,7 +55,7 @@ HidService* HidService::GetInstance(
#elif defined(OS_WIN)
g_service = new HidServiceWin();
#endif
- if (g_service != NULL) {
+ if (g_service != nullptr) {
Destroyer* destroyer = new Destroyer(g_service);
base::MessageLoop::current()->AddDestructionObserver(destroyer);
}
@@ -63,6 +63,13 @@ HidService* HidService::GetInstance(
return g_service;
}
+void HidService::SetInstanceForTest(HidService* instance) {
+ DCHECK(!g_service);
+ g_service = instance;
+ Destroyer* destroyer = new Destroyer(g_service);
+ base::MessageLoop::current()->AddDestructionObserver(destroyer);
+}
+
HidService::~HidService() {
DCHECK(thread_checker_.CalledOnValidThread());
}
diff --git a/device/hid/hid_service.h b/device/hid/hid_service.h
index a3f7132..c01fcbc 100644
--- a/device/hid/hid_service.h
+++ b/device/hid/hid_service.h
@@ -27,6 +27,8 @@ class HidService {
scoped_refptr<base::SingleThreadTaskRunner> file_task_runner,
scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner);
+ static void SetInstanceForTest(HidService* instance);
+
// Enumerates and returns a list of device identifiers.
virtual void GetDevices(std::vector<HidDeviceInfo>* devices);
diff --git a/extensions/browser/api/hid/hid_apitest.cc b/extensions/browser/api/hid/hid_apitest.cc
new file mode 100644
index 0000000..ad91648
--- /dev/null
+++ b/extensions/browser/api/hid/hid_apitest.cc
@@ -0,0 +1,187 @@
+// Copyright 2014 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 "base/run_loop.h"
+#include "base/thread_task_runner_handle.h"
+#include "content/public/browser/browser_thread.h"
+#include "device/hid/hid_collection_info.h"
+#include "device/hid/hid_connection.h"
+#include "device/hid/hid_device_info.h"
+#include "device/hid/hid_service.h"
+#include "device/hid/hid_usage_and_page.h"
+#include "extensions/shell/test/shell_apitest.h"
+#include "net/base/io_buffer.h"
+
+namespace extensions {
+
+namespace {
+
+using base::ThreadTaskRunnerHandle;
+using content::BrowserThread;
+using device::HidCollectionInfo;
+using device::HidConnection;
+using device::HidDeviceId;
+using device::HidDeviceInfo;
+using device::HidService;
+using device::HidUsageAndPage;
+using net::IOBuffer;
+
+class MockHidConnection : public HidConnection {
+ public:
+ MockHidConnection(const HidDeviceInfo& device_info)
+ : HidConnection(device_info) {}
+
+ void PlatformClose() override {}
+
+ void PlatformRead(const ReadCallback& callback) override {
+ const char kResult[] = "This is a HID input report.";
+ uint8_t report_id = device_info().has_report_id ? 1 : 0;
+ scoped_refptr<IOBuffer> buffer(new IOBuffer(sizeof(kResult)));
+ buffer->data()[0] = report_id;
+ memcpy(buffer->data() + 1, kResult, sizeof(kResult) - 1);
+ ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE, base::Bind(callback, true, buffer, sizeof(kResult)));
+ }
+
+ void PlatformWrite(scoped_refptr<net::IOBuffer> buffer,
+ size_t size,
+ const WriteCallback& callback) override {
+ const char kExpected[] = "This is a HID output report.";
+ bool result = false;
+ if (size == sizeof(kExpected)) {
+ uint8_t report_id = buffer->data()[0];
+ uint8_t expected_report_id = device_info().has_report_id ? 1 : 0;
+ if (report_id == expected_report_id) {
+ if (memcmp(buffer->data() + 1, kExpected, sizeof(kExpected) - 1) == 0) {
+ result = true;
+ }
+ }
+ }
+ ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+ base::Bind(callback, result));
+ }
+
+ void PlatformGetFeatureReport(uint8_t report_id,
+ const ReadCallback& callback) override {
+ const char kResult[] = "This is a HID feature report.";
+ scoped_refptr<IOBuffer> buffer(new IOBuffer(sizeof(kResult)));
+ size_t offset = 0;
+ if (device_info().has_report_id) {
+ buffer->data()[offset++] = report_id;
+ }
+ memcpy(buffer->data() + offset, kResult, sizeof(kResult) - 1);
+ ThreadTaskRunnerHandle::Get()->PostTask(
+ FROM_HERE,
+ base::Bind(callback, true, buffer, sizeof(kResult) - 1 + offset));
+ }
+
+ void PlatformSendFeatureReport(scoped_refptr<net::IOBuffer> buffer,
+ size_t size,
+ const WriteCallback& callback) override {
+ const char kExpected[] = "The app is setting this HID feature report.";
+ bool result = false;
+ if (size == sizeof(kExpected)) {
+ uint8_t report_id = buffer->data()[0];
+ uint8_t expected_report_id = device_info().has_report_id ? 1 : 0;
+ if (report_id == expected_report_id &&
+ memcmp(buffer->data() + 1, kExpected, sizeof(kExpected) - 1) == 0) {
+ result = true;
+ }
+ }
+ ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+ base::Bind(callback, result));
+ }
+
+ private:
+ ~MockHidConnection() {}
+};
+
+class MockHidService : public HidService {
+ public:
+ MockHidService() : HidService() {
+ {
+ HidDeviceInfo device_info;
+ device_info.device_id = "Device A";
+ device_info.vendor_id = 0x18D1;
+ device_info.product_id = 0x58F0;
+ device_info.max_input_report_size = 128;
+ device_info.max_output_report_size = 128;
+ device_info.max_feature_report_size = 128;
+ {
+ HidCollectionInfo collection_info;
+ device_info.collections.push_back(collection_info);
+ }
+ AddDevice(device_info);
+ }
+
+ {
+ HidDeviceInfo device_info;
+ device_info.device_id = "Device B";
+ device_info.vendor_id = 0x18D1;
+ device_info.product_id = 0x58F0;
+ device_info.max_input_report_size = 128;
+ device_info.max_output_report_size = 128;
+ device_info.max_feature_report_size = 128;
+ {
+ HidCollectionInfo collection_info;
+ collection_info.usage =
+ HidUsageAndPage(0, HidUsageAndPage::kPageVendor);
+ collection_info.report_ids.insert(1);
+ device_info.has_report_id = true;
+ device_info.collections.push_back(collection_info);
+ }
+ AddDevice(device_info);
+ }
+
+ {
+ HidDeviceInfo device_info;
+ device_info.device_id = "Device C";
+ device_info.vendor_id = 0x18D1;
+ device_info.product_id = 0x58F1;
+ device_info.max_input_report_size = 128;
+ device_info.max_output_report_size = 128;
+ device_info.max_feature_report_size = 128;
+ {
+ HidCollectionInfo collection_info;
+ device_info.collections.push_back(collection_info);
+ }
+ AddDevice(device_info);
+ }
+ }
+
+ void Connect(const HidDeviceId& device_id,
+ const ConnectCallback& callback) override {
+ const auto& device_entry = devices().find(device_id);
+ scoped_refptr<HidConnection> connection;
+ if (device_entry != devices().end()) {
+ connection = new MockHidConnection(device_entry->second);
+ }
+
+ ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
+ base::Bind(callback, connection));
+ }
+};
+
+} // namespace
+
+class HidApiTest : public ShellApiTest {
+ public:
+ void SetUpOnMainThread() override {
+ ShellApiTest::SetUpOnMainThread();
+ base::RunLoop run_loop;
+ BrowserThread::PostTaskAndReply(BrowserThread::FILE,
+ FROM_HERE,
+ base::Bind(&HidApiTest::SetUpService, this),
+ run_loop.QuitClosure());
+ run_loop.Run();
+ }
+
+ void SetUpService() { HidService::SetInstanceForTest(new MockHidService()); }
+};
+
+IN_PROC_BROWSER_TEST_F(HidApiTest, HidApp) {
+ ASSERT_TRUE(RunAppTest("api_test/hid/api")) << message_;
+}
+
+} // namespace extensions
diff --git a/extensions/shell/app_shell.gyp b/extensions/shell/app_shell.gyp
index 6a48006..d641a2e 100644
--- a/extensions/shell/app_shell.gyp
+++ b/extensions/shell/app_shell.gyp
@@ -195,6 +195,7 @@
# TODO(yoz): Something is off here; should this .gyp file be
# in the parent directory? Test target extensions_browsertests?
'../browser/api/dns/dns_apitest.cc',
+ '../browser/api/hid/hid_apitest.cc',
'../browser/api/socket/socket_apitest.cc',
'../browser/api/sockets_tcp/sockets_tcp_apitest.cc',
'../browser/api/sockets_udp/sockets_udp_apitest.cc',
diff --git a/extensions/test/data/api_test/hid/api/background.js b/extensions/test/data/api_test/hid/api/background.js
new file mode 100644
index 0000000..7bdd1eb
--- /dev/null
+++ b/extensions/test/data/api_test/hid/api/background.js
@@ -0,0 +1,363 @@
+// Copyright 2014 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.
+
+var kInvalidDeviceId = -1;
+var kInvalidConnectionId = -1;
+
+function getDevice(wantReportIds, callback) {
+ chrome.hid.getDevices({}, function (devices) {
+ chrome.test.assertNoLastError();
+ for (var device of devices) {
+ chrome.test.assertTrue(device.collections.length > 0);
+ var foundReportId = false;
+ for (var collection of device.collections) {
+ if (collection.reportIds.length > 0) {
+ foundReportId = true;
+ }
+ }
+ if (wantReportIds == foundReportId) {
+ callback(device);
+ return;
+ }
+ }
+ chrome.test.fail("No appropriate device found.");
+ });
+}
+
+function openDevice(wantReportIds, callback) {
+ getDevice(wantReportIds, function (device) {
+ chrome.hid.connect(device.deviceId, function (connection) {
+ chrome.test.assertNoLastError();
+ callback(connection.connectionId);
+ });
+ });
+}
+
+function openDeviceWithReportId(callback) {
+ return openDevice(true, callback);
+}
+
+function openDeviceWithoutReportId(callback) {
+ return openDevice(false, callback);
+}
+
+function arrayBufferToString(buffer) {
+ return String.fromCharCode.apply(null, new Uint8Array(buffer));
+}
+
+function stringToArrayBuffer(string) {
+ var buffer = new ArrayBuffer(string.length);
+ var view = new Uint8Array(buffer);
+ for (var i = 0; i < string.length; i++) {
+ view[i] = string.charCodeAt(i);
+ }
+ return buffer;
+}
+
+function testGetDevicesWithNoOptions() {
+ chrome.hid.getDevices({}, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(2, devices.length, "Expected two enumerated devices.");
+ chrome.test.succeed("Device enumeration successful.");
+ });
+};
+
+function testGetDevicesWithLegacyVidAndPid() {
+ chrome.hid.getDevices({
+ 'vendorId': 0x18D1,
+ 'productId': 0x58F0
+ }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(2, devices.length, "Expected two enumerated devices.");
+ chrome.test.succeed("Device enumeration successful.");
+ });
+};
+
+function testGetDevicesWithNoFilters() {
+ chrome.hid.getDevices({ 'filters': [] }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(2, devices.length, "Expected two enumerated devices.");
+ chrome.test.succeed("Device enumeration successful.");
+ });
+};
+
+function testGetDevicesWithVidPidFilter() {
+ chrome.hid.getDevices({ 'filters': [
+ { 'vendorId': 0x18D1, 'productId': 0x58F0}
+ ] }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(2, devices.length, "Expected two enumerated devices.");
+ chrome.test.succeed("Device enumeration successful.");
+ });
+};
+
+function testGetDevicesWithUsageFilter() {
+ chrome.hid.getDevices({ 'filters': [
+ { 'usagePage': 0xFF00 } /* vendor-specified usage page */
+ ] }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(1, devices.length, "Expected one enumerated device.");
+ var device = devices[0];
+ chrome.test.assertEq(1, device.collections.length,
+ "Expected one collection.");
+ var collection = device.collections[0];
+ chrome.test.assertEq(0xFF00, collection.usagePage);
+ chrome.test.succeed("Device enumeration successful.");
+ });
+}
+
+function testGetDevicesWithUnauthorizedDevice() {
+ chrome.hid.getDevices({ 'filters': [
+ { 'vendorId': 0x18D1, 'productId': 0x58F1}
+ ] }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(0, devices.length, "Expected no enumerated devices.");
+ chrome.test.succeed("Device enumeration successful.");
+ });
+};
+
+function testConnectWithInvalidDeviceId() {
+ chrome.hid.connect(kInvalidDeviceId, function (connection) {
+ chrome.test.assertLastError("Invalid HID device ID.");
+ chrome.test.succeed("Rejected invalid device ID.");
+ });
+};
+
+function testConnectAndDisconnect() {
+ chrome.hid.getDevices({ 'filters': [
+ { 'vendorId': 0x18D1, 'productId': 0x58F0 }
+ ] }, function (devices) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertTrue(devices.length >= 1, "Expected connectable device.");
+ chrome.hid.connect(devices[0].deviceId, function (connection) {
+ chrome.test.assertNoLastError();
+ chrome.hid.disconnect(connection.connectionId, function () {
+ chrome.test.assertNoLastError();
+ chrome.test.succeed("Opened and closed device.");
+ });
+ });
+ });
+};
+
+function testDisconnectWithInvalidConnectionId() {
+ chrome.hid.disconnect(kInvalidConnectionId, function () {
+ chrome.test.assertLastError("Connection not established.");
+ chrome.test.succeed("Rejected invalid connection ID.");
+ });
+}
+
+function testReceiveWithInvalidConnectionId() {
+ chrome.hid.receive(kInvalidConnectionId, function (reportId, data) {
+ chrome.test.assertLastError("Connection not established.");
+ chrome.test.succeed("Rejected invalid connection ID.");
+ });
+}
+
+function testReceiveWithReportId() {
+ openDeviceWithReportId(function (connection) {
+ chrome.hid.receive(connection, function (reportId, data) {
+ chrome.test.assertEq(1, reportId, "Expected report_id == 1.");
+ var expected = "This is a HID input report.";
+ chrome.test.assertEq(expected, arrayBufferToString(data));
+ chrome.test.succeed("Receive successful.");
+ });
+ });
+}
+
+function testReceiveWithoutReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ chrome.hid.receive(connection, function (reportId, data) {
+ chrome.test.assertNoLastError();
+ chrome.test.assertEq(0, reportId, "Expected report_id == 0.");
+ var expected = "This is a HID input report.";
+ chrome.test.assertEq(expected, arrayBufferToString(data));
+ chrome.test.succeed("Receive successful.");
+ });
+ });
+}
+
+function testSendWithInvalidConnectionId() {
+ var buffer = new ArrayBuffer();
+ chrome.hid.send(kInvalidConnectionId, 0, buffer, function () {
+ chrome.test.assertLastError("Connection not established.");
+ chrome.test.succeed("Rejected invalid connection ID.");
+ });
+}
+
+function testSendWithReportId() {
+ openDeviceWithReportId(function (connection) {
+ var buffer = stringToArrayBuffer("This is a HID output report.");
+ chrome.hid.send(connection, 1, buffer, function () {
+ chrome.test.assertNoLastError();
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Send successful.");
+ });
+ });
+}
+
+function testSendWithoutReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ var buffer = stringToArrayBuffer("This is a HID output report.");
+ chrome.hid.send(connection, 0, buffer, function () {
+ chrome.test.assertNoLastError();
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Send successful.");
+ });
+ });
+}
+
+function testSendWithInvalidReportId() {
+ openDeviceWithReportId(function (connection) {
+ var buffer = stringToArrayBuffer("This is a HID output report.");
+ chrome.hid.send(connection, 0, buffer, function () {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Caught invalid report ID.");
+ });
+ });
+}
+
+function testSendWithUnexpectedReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ var buffer = stringToArrayBuffer("This is a HID output report.");
+ chrome.hid.send(connection, 1, buffer, function () {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Caught unexpected report ID.");
+ });
+ });
+}
+
+function testReceiveFeatureReportWithInvalidConnectionId() {
+ chrome.hid.receiveFeatureReport(kInvalidConnectionId, 0, function (data) {
+ chrome.test.assertLastError("Connection not established.");
+ chrome.test.succeed("Rejected invalid connection ID.");
+ });
+}
+
+function testReceiveFeatureReportWithReportId() {
+ openDeviceWithReportId(function (connection) {
+ chrome.hid.receiveFeatureReport(connection, 1, function (data) {
+ chrome.test.assertNoLastError();
+ var expected = "\1This is a HID feature report.";
+ chrome.test.assertEq(expected, arrayBufferToString(data));
+ chrome.test.succeed("Received feature report.");
+ });
+ });
+}
+
+function testReceiveFeatureReportWithoutReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ chrome.hid.receiveFeatureReport(connection, 0, function (data) {
+ chrome.test.assertNoLastError();
+ var expected = "This is a HID feature report.";
+ chrome.test.assertEq(expected, arrayBufferToString(data));
+ chrome.test.succeed("Received feature report.");
+ });
+ });
+}
+
+function testReceiveFeatureReportWithInvalidReportId() {
+ openDeviceWithReportId(function (connection) {
+ chrome.hid.receiveFeatureReport(connection, 0, function (data) {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.test.succeed("Caught invalid report ID.");
+ });
+ });
+}
+
+function testReceiveFeatureReportWithUnexpectedReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ chrome.hid.receiveFeatureReport(connection, 1, function (data) {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.test.succeed("Caught unexpected report ID.");
+ });
+ });
+}
+
+function testSendFeatureReportWithInvalidConnectionId() {
+ var buffer = new ArrayBuffer();
+ chrome.hid.sendFeatureReport(kInvalidConnectionId, 0, buffer, function () {
+ chrome.test.assertLastError("Connection not established.");
+ chrome.test.succeed("Rejected invalid connection ID.");
+ });
+}
+
+function testSendFeatureReportWithReportId() {
+ openDeviceWithReportId(function (connection) {
+ var buffer =
+ stringToArrayBuffer("The app is setting this HID feature report.");
+ chrome.hid.sendFeatureReport(connection, 1, buffer, function () {
+ chrome.test.assertNoLastError();
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Send successful.");
+ });
+ });
+}
+
+function testSendFeatureReportWithoutReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ var buffer =
+ stringToArrayBuffer("The app is setting this HID feature report.");
+ chrome.hid.sendFeatureReport(connection, 0, buffer, function () {
+ chrome.test.assertNoLastError();
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Send successful.");
+ });
+ });
+}
+
+function testSendFeatureReportWithInvalidReportId() {
+ openDeviceWithReportId(function (connection) {
+ var buffer =
+ stringToArrayBuffer("The app is setting this HID feature report.");
+ chrome.hid.sendFeatureReport(connection, 0, buffer, function () {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Caught invalid report ID.");
+ });
+ });
+}
+
+function testSendFeatureReportWithUnexpectedReportId() {
+ openDeviceWithoutReportId(function (connection) {
+ var buffer =
+ stringToArrayBuffer("The app is setting this HID feature report.");
+ chrome.hid.sendFeatureReport(connection, 1, buffer, function () {
+ chrome.test.assertLastError("Transfer failed.");
+ chrome.hid.disconnect(connection);
+ chrome.test.succeed("Caught unexpected report ID.");
+ });
+ });
+}
+
+chrome.test.runTests([
+ testGetDevicesWithNoOptions,
+ testGetDevicesWithLegacyVidAndPid,
+ testGetDevicesWithNoFilters,
+ testGetDevicesWithVidPidFilter,
+ testGetDevicesWithUsageFilter,
+ testGetDevicesWithUnauthorizedDevice,
+ testConnectWithInvalidDeviceId,
+ testConnectAndDisconnect,
+ testDisconnectWithInvalidConnectionId,
+ testReceiveWithInvalidConnectionId,
+ testReceiveWithReportId,
+ testReceiveWithoutReportId,
+ testSendWithInvalidConnectionId,
+ testSendWithReportId,
+ testSendWithoutReportId,
+ testSendWithInvalidReportId,
+ testSendWithUnexpectedReportId,
+ testReceiveFeatureReportWithInvalidConnectionId,
+ testReceiveFeatureReportWithReportId,
+ testReceiveFeatureReportWithoutReportId,
+ testReceiveFeatureReportWithInvalidReportId,
+ testReceiveFeatureReportWithUnexpectedReportId,
+ testSendFeatureReportWithInvalidConnectionId,
+ testSendFeatureReportWithReportId,
+ testSendFeatureReportWithoutReportId,
+ testSendFeatureReportWithInvalidReportId,
+ testSendFeatureReportWithUnexpectedReportId,
+]);
diff --git a/extensions/test/data/api_test/hid/api/manifest.json b/extensions/test/data/api_test/hid/api/manifest.json
new file mode 100644
index 0000000..e17391b
--- /dev/null
+++ b/extensions/test/data/api_test/hid/api/manifest.json
@@ -0,0 +1,15 @@
+{
+ "name": "chrome.hid",
+ "version": "0.1",
+ "description": "end-to-end browser tests for chrome.hid API",
+ "app": {
+ "background": {
+ "scripts": ["background.js"]
+ }
+ },
+ "permissions": [
+ "hid",
+ // This is a test device emulated by the mocks enabled for the test.
+ { "usbDevices": [{ "vendorId": 6353, "productId": 22768 }]}
+ ]
+}