blob: b5985010c18214251c0835c8ef45604469ec46b4 [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 "src/ui/a11y/lib/annotation/annotation_view.h"
#include <fuchsia/ui/annotation/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl_test_base.h>
#include <lib/gtest/test_loop_fixture.h>
#include <lib/sys/cpp/testing/component_context_provider.h>
#include <lib/sys/cpp/testing/fake_component.h>
#include <set>
#include <unordered_map>
#include <vector>
#include <gtest/gtest.h>
#include "src/ui/a11y/lib/util/util.h"
namespace a11y {
namespace {
static constexpr fuchsia::ui::gfx::ViewProperties kViewProperties = {
.bounding_box = {.min = {.x = 10, .y = 5, .z = -100}, .max = {.x = 100, .y = 50, .z = 0}}};
struct ViewAttributes {
uint32_t id;
std::set<uint32_t> children;
bool operator==(const ViewAttributes& rhs) const {
return this->id == rhs.id && this->children == rhs.children;
}
};
struct EntityNodeAttributes {
uint32_t id;
uint32_t parent_id;
std::array<float, 3> scale_vector;
std::array<float, 3> translation_vector;
std::set<uint32_t> children;
bool operator==(const EntityNodeAttributes& rhs) const {
return this->id == rhs.id && this->parent_id == rhs.parent_id &&
this->scale_vector == rhs.scale_vector &&
this->translation_vector == rhs.translation_vector && this->children == rhs.children;
}
};
struct RectangleNodeAttributes {
uint32_t id;
uint32_t parent_id;
uint32_t rectangle_id;
uint32_t material_id;
bool operator==(const RectangleNodeAttributes& rhs) const {
return this->id == rhs.id && this->parent_id == rhs.parent_id &&
this->rectangle_id == rhs.rectangle_id && this->material_id == rhs.material_id;
}
};
struct RectangleAttributes {
uint32_t id;
uint32_t parent_id;
float width;
float height;
float elevation;
float center_x;
float center_y;
bool operator==(const RectangleAttributes& rhs) const {
return this->id == rhs.id && this->parent_id == rhs.parent_id && this->width == rhs.width &&
this->height == rhs.height && this->elevation == rhs.elevation &&
this->center_x == rhs.center_x && this->center_y == rhs.center_y;
}
};
class MockAnnotationRegistry : public fuchsia::ui::annotation::Registry {
public:
MockAnnotationRegistry() = default;
~MockAnnotationRegistry() override = default;
void CreateAnnotationViewHolder(
fuchsia::ui::views::ViewRef client_view,
fuchsia::ui::views::ViewHolderToken view_holder_token,
fuchsia::ui::annotation::Registry::CreateAnnotationViewHolderCallback callback) override {
create_annotation_view_holder_called_ = true;
callback();
}
fidl::InterfaceRequestHandler<fuchsia::ui::annotation::Registry> GetHandler(
async_dispatcher_t* dispatcher = nullptr) {
return [this, dispatcher](fidl::InterfaceRequest<fuchsia::ui::annotation::Registry> request) {
bindings_.AddBinding(this, std::move(request), dispatcher);
};
}
bool create_annotation_view_holder_called() { return create_annotation_view_holder_called_; }
private:
fidl::BindingSet<fuchsia::ui::annotation::Registry> bindings_;
bool create_annotation_view_holder_called_;
};
class MockSession : public fuchsia::ui::scenic::testing::Session_TestBase {
public:
MockSession() : binding_(this) {}
~MockSession() override = default;
void NotImplemented_(const std::string& name) override {}
void Enqueue(std::vector<fuchsia::ui::scenic::Command> cmds) override {
cmd_queue_.insert(cmd_queue_.end(), std::make_move_iterator(cmds.begin()),
std::make_move_iterator(cmds.end()));
}
void ApplyCreateResourceCommand(const fuchsia::ui::gfx::CreateResourceCmd& command) {
const uint32_t id = command.id;
switch (command.resource.Which()) {
case fuchsia::ui::gfx::ResourceArgs::Tag::kView3:
views_[id].id = id;
break;
case fuchsia::ui::gfx::ResourceArgs::Tag::kEntityNode:
entity_nodes_[id].id = id;
break;
case fuchsia::ui::gfx::ResourceArgs::Tag::kShapeNode:
rectangle_nodes_[id].id = id;
break;
case fuchsia::ui::gfx::ResourceArgs::Tag::kMaterial:
materials_.emplace(id);
break;
case fuchsia::ui::gfx::ResourceArgs::Tag::kRectangle:
EXPECT_GE(id, 8u);
rectangles_[id].id = id;
rectangles_[id].width = command.resource.rectangle().width.vector1();
rectangles_[id].height = command.resource.rectangle().height.vector1();
break;
default:
break;
}
}
void ApplyAddChildCommand(const fuchsia::ui::gfx::AddChildCmd& command) {
const uint32_t parent_id = command.node_id;
const uint32_t child_id = command.child_id;
// Update parent's children. Only views and entity nodes will have children. Also, resource ids
// are unique globally across all resource types, so only one of views_ and entity_nodes_ will
// contain parent_id as a key.
if (views_.find(parent_id) != views_.end()) {
views_[parent_id].children.insert(child_id);
} else if (entity_nodes_.find(parent_id) != entity_nodes_.end()) {
entity_nodes_[parent_id].children.insert(child_id);
}
// Update child's parent. Only entity nodes and shape nodes will have parents.
if (entity_nodes_.find(child_id) != entity_nodes_.end()) {
entity_nodes_[child_id].parent_id = parent_id;
} else if (rectangle_nodes_.find(child_id) != rectangle_nodes_.end()) {
rectangle_nodes_[child_id].parent_id = parent_id;
}
}
void ApplySetMaterialCommand(const fuchsia::ui::gfx::SetMaterialCmd& command) {
rectangle_nodes_[command.node_id].material_id = command.material_id;
}
void ApplySetShapeCommand(const fuchsia::ui::gfx::SetShapeCmd& command) {
const uint32_t node_id = command.node_id;
const uint32_t rectangle_id = command.shape_id;
rectangle_nodes_[node_id].rectangle_id = rectangle_id;
rectangles_[rectangle_id].parent_id = node_id;
}
void ApplySetTranslationCommand(const fuchsia::ui::gfx::SetTranslationCmd& command) {
if (command.id == AnnotationView::kContentNodeId) {
entity_nodes_[command.id].translation_vector[0] = command.value.value.x;
entity_nodes_[command.id].translation_vector[1] = command.value.value.y;
entity_nodes_[command.id].translation_vector[2] = command.value.value.z;
} else {
const uint32_t parent_id = command.id;
const uint32_t rectangle_id = rectangle_nodes_[parent_id].rectangle_id;
const auto& translation = command.value.value;
rectangles_[rectangle_id].center_x = translation.x;
rectangles_[rectangle_id].center_y = translation.y;
rectangles_[rectangle_id].elevation = translation.z;
}
}
void ApplySetScaleCommand(const fuchsia::ui::gfx::SetScaleCmd& command) {
if (entity_nodes_.find(command.id) != entity_nodes_.end()) {
entity_nodes_[command.id].scale_vector[0] = command.value.value.x;
entity_nodes_[command.id].scale_vector[1] = command.value.value.y;
entity_nodes_[command.id].scale_vector[2] = command.value.value.z;
}
}
void ApplyDetachCommand(const fuchsia::ui::gfx::DetachCmd& command) {
const uint32_t id = command.id;
// The annotation view only ever detaches the content entity node from the view node.
auto& entity_node = entity_nodes_[id];
if (entity_node.parent_id != 0) {
views_[entity_node.parent_id].children.erase(id);
}
entity_node.parent_id = 0u;
}
void Present(uint64_t presentation_time, ::std::vector<::zx::event> acquire_fences,
::std::vector<::zx::event> release_fences, PresentCallback callback) override {
EXPECT_FALSE(cmd_queue_.empty());
for (const auto& command : cmd_queue_) {
if (command.Which() != fuchsia::ui::scenic::Command::Tag::kGfx) {
continue;
}
const auto& gfx_command = command.gfx();
switch (gfx_command.Which()) {
case fuchsia::ui::gfx::Command::Tag::kCreateResource:
ApplyCreateResourceCommand(gfx_command.create_resource());
break;
case fuchsia::ui::gfx::Command::Tag::kAddChild:
ApplyAddChildCommand(gfx_command.add_child());
break;
case fuchsia::ui::gfx::Command::Tag::kSetMaterial:
ApplySetMaterialCommand(gfx_command.set_material());
break;
case fuchsia::ui::gfx::Command::Tag::kSetShape:
ApplySetShapeCommand(gfx_command.set_shape());
break;
case fuchsia::ui::gfx::Command::Tag::kSetTranslation:
ApplySetTranslationCommand(gfx_command.set_translation());
break;
case fuchsia::ui::gfx::Command::Tag::kSetScale:
ApplySetScaleCommand(gfx_command.set_scale());
break;
case fuchsia::ui::gfx::Command::Tag::kDetach:
ApplyDetachCommand(gfx_command.detach());
break;
default:
break;
}
}
callback(fuchsia::images::PresentationInfo());
}
void SendGfxEvent(fuchsia::ui::gfx::Event event) {
fuchsia::ui::scenic::Event scenic_event;
scenic_event.set_gfx(std::move(event));
std::vector<fuchsia::ui::scenic::Event> events;
events.emplace_back(std::move(scenic_event));
listener_->OnScenicEvent(std::move(events));
}
void SendViewPropertiesChangedEvent() {
fuchsia::ui::gfx::ViewPropertiesChangedEvent view_properties_changed_event = {
.view_id = 1u,
.properties = kViewProperties,
};
fuchsia::ui::gfx::Event event;
event.set_view_properties_changed(view_properties_changed_event);
SendGfxEvent(std::move(event));
}
void SendViewDetachedFromSceneEvent() {
fuchsia::ui::gfx::ViewDetachedFromSceneEvent view_detached_from_scene_event = {.view_id = 1u};
fuchsia::ui::gfx::Event event;
event.set_view_detached_from_scene(view_detached_from_scene_event);
SendGfxEvent(std::move(event));
}
void SendViewAttachedToSceneEvent() {
fuchsia::ui::gfx::ViewAttachedToSceneEvent view_attached_to_scene_event = {.view_id = 1u};
fuchsia::ui::gfx::Event event;
event.set_view_attached_to_scene(view_attached_to_scene_event);
SendGfxEvent(std::move(event));
}
void Bind(fidl::InterfaceRequest<::fuchsia::ui::scenic::Session> request,
::fuchsia::ui::scenic::SessionListenerPtr listener) {
binding_.Bind(std::move(request));
listener_ = std::move(listener);
}
const std::set<uint32_t>& materials() { return materials_; }
const std::unordered_map<uint32_t, ViewAttributes>& views() { return views_; }
const std::unordered_map<uint32_t, EntityNodeAttributes>& entity_nodes() { return entity_nodes_; }
const std::unordered_map<uint32_t, RectangleNodeAttributes>& rectangle_nodes() {
return rectangle_nodes_;
}
const std::unordered_map<uint32_t, RectangleAttributes>& rectangles() { return rectangles_; }
private:
fidl::Binding<fuchsia::ui::scenic::Session> binding_;
fuchsia::ui::scenic::SessionListenerPtr listener_;
std::vector<fuchsia::ui::scenic::Command> cmd_queue_;
std::set<uint32_t> materials_;
std::unordered_map<uint32_t, ViewAttributes> views_;
std::unordered_map<uint32_t, EntityNodeAttributes> entity_nodes_;
std::unordered_map<uint32_t, RectangleNodeAttributes> rectangle_nodes_;
std::unordered_map<uint32_t, RectangleAttributes> rectangles_;
};
class FakeScenic : public fuchsia::ui::scenic::testing::Scenic_TestBase {
public:
explicit FakeScenic(MockSession* mock_session) : mock_session_(mock_session) {}
~FakeScenic() override = default;
void NotImplemented_(const std::string& name) override {}
void CreateSession(
fidl::InterfaceRequest<fuchsia::ui::scenic::Session> session,
fidl::InterfaceHandle<fuchsia::ui::scenic::SessionListener> listener) override {
mock_session_->Bind(std::move(session), listener.Bind());
create_session_called_ = true;
}
fidl::InterfaceRequestHandler<fuchsia::ui::scenic::Scenic> GetHandler(
async_dispatcher_t* dispatcher = nullptr) {
return [this, dispatcher](fidl::InterfaceRequest<fuchsia::ui::scenic::Scenic> request) {
bindings_.AddBinding(this, std::move(request), dispatcher);
};
}
bool create_session_called() { return create_session_called_; }
private:
fidl::BindingSet<fuchsia::ui::scenic::Scenic> bindings_;
MockSession* mock_session_;
bool create_session_called_;
};
class AnnotationViewTest : public gtest::TestLoopFixture {
public:
AnnotationViewTest() = default;
~AnnotationViewTest() override = default;
void SetUp() override {
gtest::TestLoopFixture::SetUp();
mock_session_ = std::make_unique<MockSession>();
fake_scenic_ = std::make_unique<FakeScenic>(mock_session_.get());
mock_annotation_registry_ = std::make_unique<MockAnnotationRegistry>();
context_provider_.service_directory_provider()->AddService(fake_scenic_->GetHandler());
context_provider_.service_directory_provider()->AddService(
mock_annotation_registry_->GetHandler());
properties_changed_ = false;
view_attached_ = false;
view_detached_ = false;
annotation_view_factory_ = std::make_unique<AnnotationViewFactory>();
annotation_view_ = annotation_view_factory_->CreateAndInitAnnotationView(
CreateOrphanViewRef(), context_provider_.context(),
[this]() { properties_changed_ = true; }, [this]() { view_attached_ = true; },
[this]() { view_detached_ = true; });
RunLoopUntilIdle();
}
fuchsia::ui::views::ViewRef CreateOrphanViewRef() {
fuchsia::ui::views::ViewRef view_ref;
zx::eventpair::create(0u, &view_ref.reference, &eventpair_peer_);
return view_ref;
}
void ExpectView(ViewAttributes expected) {
const auto& views = mock_session_->views();
EXPECT_EQ(views.at(expected.id), expected);
}
void ExpectMaterial(uint32_t expected) {
const auto& materials = mock_session_->materials();
EXPECT_NE(materials.find(expected), materials.end());
}
void ExpectEntityNode(EntityNodeAttributes expected) {
const auto& entity_nodes = mock_session_->entity_nodes();
EXPECT_EQ(entity_nodes.at(expected.id), expected);
}
void ExpectRectangleNode(RectangleNodeAttributes expected) {
const auto& rectangle_nodes = mock_session_->rectangle_nodes();
EXPECT_EQ(rectangle_nodes.at(expected.id), expected);
}
void ExpectRectangle(RectangleAttributes expected) {
const auto& rectangles = mock_session_->rectangles();
EXPECT_EQ(rectangles.at(expected.id), expected);
}
void ExpectHighlightEdge(uint32_t id, uint32_t parent_id, float width, float height,
float center_x, float center_y, float elevation) {
// Check properties for rectangle shape.
RectangleAttributes rectangle;
rectangle.id = id;
rectangle.parent_id = parent_id;
rectangle.width = width;
rectangle.height = height;
rectangle.center_x = center_x;
rectangle.center_y = center_y;
rectangle.elevation = elevation;
ExpectRectangle(rectangle);
// Check that rectangle was set as shape of parent node.
ExpectRectangleNode(
{parent_id, AnnotationView::kContentNodeId, id, AnnotationView::kHighlightMaterialId});
}
protected:
sys::testing::ComponentContextProvider context_provider_;
std::unique_ptr<MockSession> mock_session_;
std::unique_ptr<FakeScenic> fake_scenic_;
std::unique_ptr<MockAnnotationRegistry> mock_annotation_registry_;
zx::eventpair eventpair_peer_;
std::unique_ptr<AnnotationViewFactory> annotation_view_factory_;
std::unique_ptr<AnnotationViewInterface> annotation_view_;
bool properties_changed_;
bool view_attached_;
bool view_detached_;
};
TEST_F(AnnotationViewTest, TestInit) {
EXPECT_TRUE(mock_annotation_registry_->create_annotation_view_holder_called());
// Verify that annotation view was created.
ExpectView({AnnotationView::kAnnotationViewId, {}});
// Verify that top-level content node (used to attach/detach annotations from view) was created.
ExpectEntityNode(
{AnnotationView::kContentNodeId,
0u,
{}, /* scale vector */
{}, /* translation vector */
{AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kHighlightRightEdgeNodeId,
AnnotationView::kHighlightTopEdgeNodeId, AnnotationView::kHighlightBottomEdgeNodeId}});
// Verify that drawing material was created.
ExpectMaterial(AnnotationView::kHighlightMaterialId);
// Verify that four shape nodes that will hold respective edge rectangles are created and added as
// children of top-level content node. Also verify material of each.
ExpectRectangleNode({AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kContentNodeId, 0,
AnnotationView::kHighlightMaterialId});
ExpectRectangleNode({AnnotationView::kHighlightRightEdgeNodeId, AnnotationView::kContentNodeId, 0,
AnnotationView::kHighlightMaterialId});
ExpectRectangleNode({AnnotationView::kHighlightTopEdgeNodeId, AnnotationView::kContentNodeId, 0,
AnnotationView::kHighlightMaterialId});
ExpectRectangleNode({AnnotationView::kHighlightBottomEdgeNodeId, AnnotationView::kContentNodeId,
0, AnnotationView::kHighlightMaterialId});
}
TEST_F(AnnotationViewTest, TestDrawHighlight) {
fuchsia::ui::gfx::BoundingBox bounding_box = {.min = {.x = 0, .y = 0, .z = 0},
.max = {.x = 1.0, .y = 2.0, .z = 3.0}};
annotation_view_->DrawHighlight(bounding_box, {1, 1, 1}, {0, 0, 0});
RunLoopUntilIdle();
// Verify that all four expected edges are present.
// Resource IDs 1-7 are used for the resources created in InitializeView(), so the next available
// id is 8. Since resource ids are generated incrementally, we expect the four edge rectangles to
// have ids 8-11.
// Before we set up the parent View bounding box, the z value of default
// bounding box is 0.
constexpr float kHighlightElevation = 0.0f;
ExpectHighlightEdge(
8u, AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kHighlightEdgeThickness,
bounding_box.max.y + AnnotationView::kHighlightEdgeThickness, bounding_box.min.x,
(bounding_box.min.y + bounding_box.max.y) / 2, kHighlightElevation);
ExpectHighlightEdge(
9u, AnnotationView::kHighlightRightEdgeNodeId, AnnotationView::kHighlightEdgeThickness,
bounding_box.max.y + AnnotationView::kHighlightEdgeThickness, bounding_box.max.x,
(bounding_box.min.y + bounding_box.max.y) / 2.f, kHighlightElevation);
ExpectHighlightEdge(10u, AnnotationView::kHighlightTopEdgeNodeId,
bounding_box.max.x + AnnotationView::kHighlightEdgeThickness,
AnnotationView::kHighlightEdgeThickness,
(bounding_box.min.x + bounding_box.max.x) / 2.f, bounding_box.max.y,
kHighlightElevation);
ExpectHighlightEdge(11u, AnnotationView::kHighlightBottomEdgeNodeId,
bounding_box.max.x + AnnotationView::kHighlightEdgeThickness,
AnnotationView::kHighlightEdgeThickness,
(bounding_box.min.x + bounding_box.max.x) / 2.f, bounding_box.min.y,
kHighlightElevation);
// Verify that top-level content node (used to attach/detach annotations from view) was attached
// to view.
ExpectEntityNode(
{AnnotationView::kContentNodeId,
AnnotationView::kAnnotationViewId,
{1, 1, 1}, /* scale vector */
{0, 0, 0}, /* translation vector */
{AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kHighlightRightEdgeNodeId,
AnnotationView::kHighlightTopEdgeNodeId, AnnotationView::kHighlightBottomEdgeNodeId}});
}
TEST_F(AnnotationViewTest, TestDetachViewContents) {
fuchsia::ui::gfx::BoundingBox bounding_box = {.min = {.x = 0, .y = 0, .z = 0},
.max = {.x = 1.0, .y = 2.0, .z = 3.0}};
annotation_view_->DrawHighlight(bounding_box, {1, 1, 1}, {0, 0, 0});
RunLoopUntilIdle();
// Verify that top-level content node (used to attach/detach annotations from view) was attached
// to view.
ExpectEntityNode(
{AnnotationView::kContentNodeId,
AnnotationView::kAnnotationViewId,
{1, 1, 1}, /* scale vector */
{0, 0, 0}, /* translation vector */
{AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kHighlightRightEdgeNodeId,
AnnotationView::kHighlightTopEdgeNodeId, AnnotationView::kHighlightBottomEdgeNodeId}});
annotation_view_->DetachViewContents();
RunLoopUntilIdle();
// Verify that top-level content node (used to attach/detach annotations from view) was detached
// from view.
ExpectEntityNode(
{AnnotationView::kContentNodeId,
0u,
{1, 1, 1}, /* scale vector */
{0, 0, 0}, /* translation vector */
{AnnotationView::kHighlightLeftEdgeNodeId, AnnotationView::kHighlightRightEdgeNodeId,
AnnotationView::kHighlightTopEdgeNodeId, AnnotationView::kHighlightBottomEdgeNodeId}});
}
TEST_F(AnnotationViewTest, TestViewPropertiesChangedEvent) {
fuchsia::ui::gfx::BoundingBox bounding_box = {.min = {.x = 0, .y = 0, .z = 0},
.max = {.x = 1.0, .y = 2.0, .z = 3.0}};
annotation_view_->DrawHighlight(bounding_box, {1, 1, 1}, {0, 0, 0});
RunLoopUntilIdle();
// Update test node bounding box to reflect change in view properties.
bounding_box = {.min = {.x = 0, .y = 0, .z = 0}, .max = {.x = 2.0, .y = 4.0, .z = 6.0}};
mock_session_->SendViewPropertiesChangedEvent();
RunLoopUntilIdle();
EXPECT_TRUE(properties_changed_);
}
TEST_F(AnnotationViewTest, TestViewPropertiesChangedElevation) {
mock_session_->SendViewPropertiesChangedEvent();
RunLoopUntilIdle();
fuchsia::ui::gfx::BoundingBox bounding_box = {.min = {.x = 0, .y = 0, .z = 0},
.max = {.x = 1.0, .y = 2.0, .z = 3.0}};
annotation_view_->DrawHighlight(bounding_box, {1, 1, 1}, {0, 0, 0});
RunLoopUntilIdle();
// Same as the value defined in annotation_view.cc.
const float kEpsilon = 0.950f;
const float kExpectedElevation = kViewProperties.bounding_box.min.z * kEpsilon;
const auto& rectangles = mock_session_->rectangles();
EXPECT_FLOAT_EQ(rectangles.at(8u).elevation, kExpectedElevation);
EXPECT_FLOAT_EQ(rectangles.at(9u).elevation, kExpectedElevation);
EXPECT_FLOAT_EQ(rectangles.at(10u).elevation, kExpectedElevation);
EXPECT_FLOAT_EQ(rectangles.at(11u).elevation, kExpectedElevation);
EXPECT_TRUE(properties_changed_);
}
TEST_F(AnnotationViewTest, TestViewDetachAndReattachEvents) {
fuchsia::ui::gfx::BoundingBox bounding_box = {.min = {.x = 0, .y = 0, .z = 0},
.max = {.x = 1.0, .y = 2.0, .z = 3.0}};
annotation_view_->DrawHighlight(bounding_box, {1, 1, 1}, {0, 0, 0});
// ViewAttachedToSceneEvent() should have no effect before any highlights are drawn.
mock_session_->SendViewDetachedFromSceneEvent();
RunLoopUntilIdle();
EXPECT_TRUE(view_detached_);
mock_session_->SendViewAttachedToSceneEvent();
RunLoopUntilIdle();
EXPECT_TRUE(view_attached_);
}
} // namespace
} // namespace a11y