chromium/chrome/browser/extensions/api/braille_display_private/braille_display_private_apitest.cc

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stddef.h>

#include "base/containers/circular_deque.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/login/lock/screen_locker.h"
#include "chrome/browser/ash/login/lock/screen_locker_tester.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/api/braille_display_private/braille_controller_brlapi.h"
#include "chrome/browser/extensions/api/braille_display_private/braille_display_private_api.h"
#include "chrome/browser/extensions/api/braille_display_private/brlapi_connection.h"
#include "chrome/browser/extensions/api/braille_display_private/stub_braille_controller.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/test/base/testing_profile.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"

namespace extensions {
namespace api {
namespace braille_display_private {

namespace {

constexpr char kTestUserEmail[] = "[email protected]";

// Used to make ReadKeys return an error.
brlapi_keyCode_t kErrorKeyCode = BRLAPI_KEY_MAX;

}  // namespace

// Data maintained by the mock BrlapiConnection.  This data lives throughout
// a test, while the api implementation takes ownership of the connection
// itself.
struct MockBrlapiConnectionData {
  bool connected;
  size_t display_columns;
  size_t display_rows;
  size_t cell_size;
  brlapi_error_t error;
  std::vector<std::string> written_content;
  // List of brlapi key codes.  A negative number makes the connection mock
  // return an error from ReadKey.
  base::circular_deque<brlapi_keyCode_t> pending_keys;
  // Causes a new display to appear to appear on disconnect, that is the
  // display size doubles and the controller gets notified of a brltty
  // restart.
  bool reappear_on_disconnect;
};

class MockBrlapiConnection : public BrlapiConnection {
 public:
  explicit MockBrlapiConnection(MockBrlapiConnectionData* data)
      : data_(data) {}

  ConnectResult Connect(OnDataReadyCallback on_data_ready) override {
    data_->connected = true;
    on_data_ready_ = std::move(on_data_ready);
    if (!data_->pending_keys.empty()) {
      content::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(&MockBrlapiConnection::NotifyDataReady,
                                    base::Unretained(this)));
    }
    return CONNECT_SUCCESS;
  }

  void Disconnect() override {
    data_->connected = false;
    if (data_->reappear_on_disconnect) {
      data_->display_columns *= 2;
      content::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              &BrailleControllerImpl::PokeSocketDirForTesting,
              base::Unretained(BrailleControllerImpl::GetInstance())));
    }
  }

  bool Connected() override { return data_->connected; }

  brlapi_error_t* BrlapiError() override { return &data_->error; }

  std::string BrlapiStrError() override {
    return data_->error.brlerrno != BRLAPI_ERROR_SUCCESS ? "Error" : "Success";
  }

  bool GetDisplaySize(unsigned int* columns, unsigned int* rows) override {
    *columns = data_->display_columns;
    *rows = data_->display_rows;
    return true;
  }

  bool WriteDots(const std::vector<unsigned char>& cells) override {
    std::string written(
        cells.begin(),
        cells.begin() + data_->display_rows * data_->display_columns);
    data_->written_content.push_back(written);
    return true;
  }

  int ReadKey(brlapi_keyCode_t* key_code) override {
    if (!data_->pending_keys.empty()) {
      brlapi_keyCode_t queued_key_code = data_->pending_keys.front();
      data_->pending_keys.pop_front();
      if (queued_key_code == kErrorKeyCode) {
        data_->error.brlerrno = BRLAPI_ERROR_EOF;
        return -1;  // Signal error.
      }
      *key_code = queued_key_code;
      return 1;
    } else {
      return 0;
    }
  }

  bool GetCellSize(unsigned int* cell_size) override {
    *cell_size = data_->cell_size;
    return true;
  }

 private:
  void NotifyDataReady() {
    on_data_ready_.Run();
    if (!data_->pending_keys.empty()) {
      content::GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(&MockBrlapiConnection::NotifyDataReady,
                                    base::Unretained(this)));
    }
  }

  raw_ptr<MockBrlapiConnectionData, LeakedDanglingUntriaged> data_;
  OnDataReadyCallback on_data_ready_;
};

class BrailleDisplayPrivateApiTest : public ExtensionApiTest {
 public:
  void SetUpInProcessBrowserTestFixture() override {
    ExtensionApiTest::SetUpInProcessBrowserTestFixture();
    connection_data_.connected = false;
    connection_data_.display_rows = 0;
    connection_data_.display_columns = 0;
    connection_data_.error.brlerrno = BRLAPI_ERROR_SUCCESS;
    connection_data_.reappear_on_disconnect = false;
    BrailleControllerImpl::GetInstance()->use_self_in_tests_ = true;
    BrailleControllerImpl::GetInstance()->SetCreateBrlapiConnectionForTesting(
        base::BindOnce(&BrailleDisplayPrivateApiTest::CreateBrlapiConnection,
                       base::Unretained(this)));
    BrailleControllerImpl::GetInstance()->skip_libbrlapi_so_load_ = true;
    DisableAccessibilityManagerBraille();
  }

 protected:
  MockBrlapiConnectionData connection_data_;

  // By default, don't let the accessibility manager interfere and
  // steal events.  Some tests override this to keep the normal behaviour
  // of the accessibility manager.
  virtual void DisableAccessibilityManagerBraille() {
    ash::AccessibilityManager::SetBrailleControllerForTest(
        &stub_braille_controller_);
  }

 private:
  std::unique_ptr<BrlapiConnection> CreateBrlapiConnection() {
    return std::unique_ptr<BrlapiConnection>(
        new MockBrlapiConnection(&connection_data_));
  }

  StubBrailleController stub_braille_controller_;
};

IN_PROC_BROWSER_TEST_F(BrailleDisplayPrivateApiTest, WriteDots) {
  connection_data_.display_columns = 11;
  connection_data_.display_rows = 1;
  connection_data_.cell_size = 6;
  ASSERT_TRUE(RunExtensionTest("braille_display_private/write_dots", {},
                               {.load_as_component = true}))
      << message_;
  ASSERT_EQ(4U, connection_data_.written_content.size());

  // testWriteEmptyCells.
  EXPECT_EQ(std::string(11, 0), connection_data_.written_content[0]);

  // testWriteOversizedCells.
  EXPECT_EQ(std::string(11, 1), connection_data_.written_content[1]);
  EXPECT_EQ(std::string(11, 2), connection_data_.written_content[2]);

  // testWriteUndersizedCellsNoCrash.
  EXPECT_EQ(std::string(9, 3) + std::string(2, 0),
            connection_data_.written_content[3]);
}

IN_PROC_BROWSER_TEST_F(BrailleDisplayPrivateApiTest, WriteDotsMultiLine) {
  connection_data_.display_columns = 20;
  connection_data_.display_rows = 7;
  connection_data_.cell_size = 6;
  ASSERT_TRUE(RunExtensionTest("braille_display_private/write_dots_multi_line",
                               {}, {.load_as_component = true}))
      << message_;
  ASSERT_EQ(6U, connection_data_.written_content.size());

  // testWriteEmptyCells.
  EXPECT_EQ(std::string(140, 0), connection_data_.written_content[0]);
  EXPECT_EQ(std::string(140, 0), connection_data_.written_content[1]);
  EXPECT_EQ(std::string(140, 0), connection_data_.written_content[2]);

  // testWriteOversizedCells.

  // The test passes a grid of cells 19x9 on a display of 20x7. (cols x rows).
  // Thus, the last two rows get truncated, and the last column is unfilled
  // (0s).
  std::string grid;
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  grid += std::string(19, 1) + std::string(1, 0);
  EXPECT_EQ(grid, connection_data_.written_content[3]);

  // This one is 21x8, so only the last row is truncated.
  EXPECT_EQ(std::string(140, 2), connection_data_.written_content[4]);

  // testWriteUndersizedCellsNoCrash.

  // 10x2.
  std::string grid2 = std::string(10, 3) + std::string(10, 0) +
                      std::string(10, 3) + std::string(10, 0) +
                      std::string(100, 0);
  EXPECT_EQ(grid2, connection_data_.written_content[5]);
}

IN_PROC_BROWSER_TEST_F(BrailleDisplayPrivateApiTest, KeyEvents) {
  connection_data_.display_columns = 11;
  connection_data_.display_rows = 1;
  connection_data_.cell_size = 6;

  // Braille navigation commands.
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_LNUP);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_LNDN);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_FWINLT);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_FWINRT);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_TOP);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_BOT);
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                          BRLAPI_KEY_CMD_ROUTE | 5);

  // Braille display standard keyboard emulation.

  // An ascii character.
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_SYM | 'A');
  // A non-ascii 'latin1' character.  Small letter a with ring above.
  connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_SYM | 0xE5);
  // A non-latin1 Unicode character.  LATIN SMALL LETTER A WITH MACRON.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_SYM_UNICODE | 0x100);
  // A Unicode character outside the BMP.  CAT FACE WITH TEARS OF JOY.
  // With anticipation for the first emoji-enabled braille display.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_SYM_UNICODE | 0x1F639);
  // Invalid Unicode character.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_SYM_UNICODE | 0x110000);

  // Non-alphanumeric function keys.

  // Backspace.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_SYM_BACKSPACE);
  // Shift+Tab.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_FLG_SHIFT | BRLAPI_KEY_SYM_TAB);
  // Alt+F3. (0-based).
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_SYM | BRLAPI_KEY_FLG_META |
      (BRLAPI_KEY_SYM_FUNCTION + 2));

  // ctrl+dot1+dot2.
  connection_data_.pending_keys.push_back(
      BRLAPI_KEY_TYPE_CMD | BRLAPI_KEY_FLG_CONTROL | BRLAPI_KEY_CMD_PASSDOTS |
      BRLAPI_DOT1 | BRLAPI_DOT2);

  // Braille dot keys, all combinations including space (0).
  for (int i = 0; i < 256; ++i) {
    connection_data_.pending_keys.push_back(BRLAPI_KEY_TYPE_CMD |
                                            BRLAPI_KEY_CMD_PASSDOTS | i);
  }

  ASSERT_TRUE(RunExtensionTest("braille_display_private/key_events", {},
                               {.load_as_component = true}));
}

IN_PROC_BROWSER_TEST_F(BrailleDisplayPrivateApiTest, DisplayStateChanges) {
  connection_data_.display_columns = 11;
  connection_data_.display_rows = 1;
  connection_data_.cell_size = 6;
  connection_data_.pending_keys.push_back(kErrorKeyCode);
  connection_data_.reappear_on_disconnect = true;
  ASSERT_TRUE(RunExtensionTest("braille_display_private/display_state_changes",
                               {}, {.load_as_component = true}));
}

class BrailleDisplayPrivateAPIUserTest : public BrailleDisplayPrivateApiTest {
 public:
  class MockEventDelegate : public BrailleDisplayPrivateAPI::EventDelegate {
   public:
    MockEventDelegate() = default;
    MockEventDelegate(const MockEventDelegate&) = delete;
    MockEventDelegate& operator=(const MockEventDelegate&) = delete;
    ~MockEventDelegate() override = default;

    int GetEventCount() const { return event_count_; }

    // BrailleDisplayPrivateAPI::EventDelegate:
    void BroadcastEvent(std::unique_ptr<Event> event) override {
      ++event_count_;
    }
    bool HasListener() override { return true; }

   private:
    int event_count_ = 0;
  };

  BrailleDisplayPrivateAPIUserTest() = default;
  BrailleDisplayPrivateAPIUserTest(const BrailleDisplayPrivateAPIUserTest&) =
      delete;
  BrailleDisplayPrivateAPIUserTest& operator=(
      const BrailleDisplayPrivateAPIUserTest&) = delete;
  ~BrailleDisplayPrivateAPIUserTest() override = default;

  MockEventDelegate* SetMockEventDelegate(BrailleDisplayPrivateAPI* api) {
    MockEventDelegate* delegate = new MockEventDelegate();
    api->SetEventDelegateForTest(
        std::unique_ptr<BrailleDisplayPrivateAPI::EventDelegate>(delegate));
    return delegate;
  }

  // BrailleDisplayPrivateApiTest:
  void SetUpInProcessBrowserTestFixture() override {
    BrailleDisplayPrivateApiTest::SetUpInProcessBrowserTestFixture();
    zero_duration_mode_ =
        std::make_unique<ui::ScopedAnimationDurationScaleMode>(
            ui::ScopedAnimationDurationScaleMode::ZERO_DURATION);
  }

  // BrailleDisplayPrivateApiTest:
  void DisableAccessibilityManagerBraille() override {
    // Let the accessibility manager behave as usual for these tests.
  }

 protected:
  std::unique_ptr<ui::ScopedAnimationDurationScaleMode> zero_duration_mode_;
};

IN_PROC_BROWSER_TEST_F(BrailleDisplayPrivateAPIUserTest, KeyEventOnLockScreen) {
  ash::ScreenLockerTester tester;

  // Make sure the signin profile and active profile are different.
  Profile* signin_profile = ash::ProfileHelper::GetSigninProfile();
  Profile* user_profile = ProfileManager::GetActiveUserProfile();
  ASSERT_FALSE(signin_profile->IsSameOrParent(user_profile))
      << signin_profile->GetDebugName() << " vs "
      << user_profile->GetDebugName();

  // Create API and event delegate for the signin profile.
  BrailleDisplayPrivateAPI signin_api(signin_profile);
  MockEventDelegate* signin_delegate = SetMockEventDelegate(&signin_api);
  EXPECT_EQ(0, signin_delegate->GetEventCount());
  // Create API and delegate for the user profile.
  BrailleDisplayPrivateAPI user_api(user_profile);
  MockEventDelegate* user_delegate = SetMockEventDelegate(&user_api);

  // Send key event to both profiles.
  KeyEvent key_event;
  key_event.command = KeyCommand::kLineUp;
  signin_api.OnBrailleKeyEvent(key_event);
  user_api.OnBrailleKeyEvent(key_event);
  EXPECT_EQ(0, signin_delegate->GetEventCount());
  EXPECT_EQ(1, user_delegate->GetEventCount());

  // Lock screen, and make sure that the key event goes to the signin profile.
  tester.Lock();
  signin_api.OnBrailleKeyEvent(key_event);
  user_api.OnBrailleKeyEvent(key_event);
  EXPECT_EQ(0, signin_delegate->GetEventCount());
  EXPECT_EQ(2, user_delegate->GetEventCount());

  // Unlock screen, making sure key events go to the user profile again.
  tester.SetUnlockPassword(AccountId::FromUserEmail(kTestUserEmail), "pass");
  tester.UnlockWithPassword(AccountId::FromUserEmail(kTestUserEmail), "pass");
  signin_api.OnBrailleKeyEvent(key_event);
  user_api.OnBrailleKeyEvent(key_event);
  EXPECT_EQ(0, signin_delegate->GetEventCount());
  EXPECT_EQ(3, user_delegate->GetEventCount());
}

}  // namespace braille_display_private
}  // namespace api
}  // namespace extensions