blob: 20acb25d7a3de7189f13ff2a4bf21142c6a144c4 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/accessibility/scoped_mode_collection.h"
#include <memory>
#include <optional>
#include "base/functional/callback_helpers.h"
#include "base/test/mock_callback.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_mode.h"
namespace content {
using ::testing::_;
using ::testing::DoAll;
using ::testing::WithArg;
using ::testing::WithoutArgs;
using MockModeChangedCallback = ::testing::StrictMock<
base::MockCallback<ScopedModeCollection::OnModeChangedCallback>>;
// An action that evaluates an expectation that `collection` is or is not
// empty.
ACTION_P(ExpectCollectionModeEqualsArg, collection) {
EXPECT_EQ(collection->accessibility_mode(), arg0);
}
class ScopedModeCollectionTest : public ::testing::Test {
protected:
ScopedModeCollectionTest() {
// Set a default action on the callback to check the invariant that the
// `new_mode` given to the callback equals the collection's notion of the
// effective mode.
ON_CALL(callback_, Run(_, _))
.WillByDefault(
WithArg<1>(ExpectCollectionModeEqualsArg(&collection())));
}
void SetUp() override {
// The mode must be empty at construction.
ASSERT_EQ(collection_->accessibility_mode(), ui::AXMode());
}
bool HasCollection() const { return collection_.has_value(); }
void PrematurelyDestroyCollection() { collection_.reset(); }
MockModeChangedCallback& callback() { return callback_; }
ScopedModeCollection& collection() { return collection_.value(); }
std::unique_ptr<ScopedAccessibilityMode>& lazy_scoped_mode() {
return lazy_scoped_mode_;
}
private:
// Must precede `collection_`, which holds its callback.
MockModeChangedCallback callback_;
// Must precede `collection_` so that it is destructed after it; see
// `OutstandingScoper` below.
std::unique_ptr<ScopedAccessibilityMode> lazy_scoped_mode_;
std::optional<ScopedModeCollection> collection_{callback_.Get()};
};
// Tests the most basic use of adding/removing a scoper.
TEST_F(ScopedModeCollectionTest, SimpleAddRemove) {
// The callback should be run twice: once when a scoper is added and again
// when it is destroyed.
{
::testing::InSequence sequence;
EXPECT_CALL(callback(), Run(ui::AXMode(), ui::kAXModeComplete));
EXPECT_CALL(callback(), Run(ui::kAXModeComplete, ui::AXMode()));
}
auto scoped_mode = collection().Add(ui::kAXModeComplete);
}
// Tests multiple scopers perfectly nested.
TEST_F(ScopedModeCollectionTest, MultipleNested) {
// The callback should be run for each addition/removal for nested scopers.
{
::testing::InSequence sequence;
EXPECT_CALL(callback(), Run(ui::AXMode(), ui::kAXModeBasic));
EXPECT_CALL(callback(), Run(ui::kAXModeBasic, ui::kAXModeComplete));
EXPECT_CALL(callback(), Run(ui::kAXModeComplete, ui::kAXModeBasic));
EXPECT_CALL(callback(), Run(ui::kAXModeBasic, ui::AXMode()));
}
auto outer_scoped_mode = collection().Add(ui::kAXModeBasic);
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeBasic);
auto inner_scoped_mode = collection().Add(ui::kAXModeComplete);
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeComplete);
inner_scoped_mode.reset();
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeBasic);
outer_scoped_mode.reset();
ASSERT_EQ(collection().accessibility_mode(), ui::AXMode());
}
// Tests multiple scopers deleted out of order.
TEST_F(ScopedModeCollectionTest, MultipleNotNested) {
// The callback should not be run when the first scoper is deleted since its
// mode flags are a subset of the second.
{
::testing::InSequence sequence;
EXPECT_CALL(callback(), Run(ui::AXMode(), ui::kAXModeBasic));
EXPECT_CALL(callback(), Run(ui::kAXModeBasic, ui::kAXModeComplete));
EXPECT_CALL(callback(), Run(ui::kAXModeComplete, ui::AXMode()));
}
auto first_scoped_mode = collection().Add(ui::kAXModeBasic);
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeBasic);
auto second_scoped_mode = collection().Add(ui::kAXModeComplete);
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeComplete);
first_scoped_mode.reset();
ASSERT_EQ(collection().accessibility_mode(), ui::kAXModeComplete);
second_scoped_mode.reset();
ASSERT_EQ(collection().accessibility_mode(), ui::AXMode());
}
// Tests that deleting the collection while it holds a scoper should neither
// crash nor run the callback when the scoper is destroyed. (Note: by
// "outstanding", we mean that there remains a scoper alive. We are not making a
// judgement about this particular scoper being better in any way than another
// scoper. All scopers are equal in the transistors of the CPU.)
TEST_F(ScopedModeCollectionTest, OutstandingScoper) {
// The callback should be run once when the scoper is added but not when it
// is destroyed.
EXPECT_CALL(callback(), Run(ui::AXMode(), ui::kAXModeComplete));
// Make sure that `scoped_mode` outlives `collection`.
lazy_scoped_mode() = collection().Add(ui::kAXModeComplete);
}
// An action that evaluates an expectation that `collection` is empty.
ACTION_P(ExpectCollectionIsEmpty, collection) {
EXPECT_TRUE(collection->empty());
}
// An action that evaluates an expectation that `collection` is not empty.
ACTION_P(ExpectCollectionIsNotEmpty, collection) {
EXPECT_FALSE(collection->empty());
}
// Tests that `empty()` works.
TEST_F(ScopedModeCollectionTest, Empty) {
// Expect that `empty()` will return the right thing from within the callback.
{
::testing::InSequence sequence;
EXPECT_CALL(callback(), Run(_, _))
.WillOnce(
DoAll(WithArg<1>(ExpectCollectionModeEqualsArg(&collection())),
WithoutArgs(ExpectCollectionIsNotEmpty(&collection()))));
EXPECT_CALL(callback(), Run(_, _))
.WillOnce(
DoAll(WithArg<1>(ExpectCollectionModeEqualsArg(&collection())),
WithoutArgs(ExpectCollectionIsEmpty(&collection()))));
}
ASSERT_TRUE(collection().empty());
auto scoped_mode = collection().Add(ui::kAXModeComplete);
ASSERT_FALSE(collection().empty());
scoped_mode.reset();
ASSERT_TRUE(collection().empty());
}
// Tests that destroying a collection from within its callback does not crash,
// even if scopers are still alive.
TEST_F(ScopedModeCollectionTest, DestroyFromCallback) {
{
::testing::InSequence sequence;
EXPECT_CALL(callback(), Run(_, _));
EXPECT_CALL(callback(), Run(_, _));
EXPECT_CALL(callback(), Run(_, _)).WillOnce(WithoutArgs([this]() {
PrematurelyDestroyCollection();
}));
}
ASSERT_TRUE(collection().empty());
auto first_scoped_mode = collection().Add(ui::kAXModeBasic);
auto second_scoped_mode = collection().Add(ui::kAXModeComplete);
// The collection will be destroyed by the callback when the mode drops back
// down to `kAXModeBasic`.
second_scoped_mode.reset();
ASSERT_FALSE(HasCollection());
}
} // namespace content