mirror of
https://github.com/aphrody-labs/cef.git
synced 2026-10-13 21:07:07 +00:00
Add per-browser ax_viewport_collapse setting
Adds role-aware collapsing of off-screen nodes to CDP accessibility tree serialization (Accessibility.getFullAXTree). When enabled, off-screen landmarks and headings are serialized as summaries (role + name only) and other off-screen nodes are pruned. Position:fixed/sticky descendants of off-screen parents are still discovered and fully serialized. Reduces snapshot size for AI agents using Playwright ariaSnapshot(). Platform screen readers are unaffected — they use a separate code path. CDP nodesUpdated events are suppressed when active. The setting is a per-browser CefBrowserSettings field threaded through WebPreferences, with a runtime override via SetAxViewportCollapse(). Both APIs are guarded with CEF_API_ADDED(CEF_EXPERIMENTAL).
This commit is contained in:
1 parent
2d766e51dd
commit
b6e302c3aa
23 files changed
+2129
-13
No files matched your search
@@ -550,6 +550,7 @@
|
||||
'ceftests_sources_common': [
|
||||
'tests/ceftests/api_version_unittest.cc',
|
||||
'tests/ceftests/audio_output_unittest.cc',
|
||||
'tests/ceftests/ax_viewport_collapse_unittest.cc',
|
||||
'tests/ceftests/base/atomic_flag_unittest.cc',
|
||||
'tests/ceftests/base/bind_unittest.cc',
|
||||
'tests/ceftests/base/callback_helpers_unittest.cc',
|
||||
|
||||
@@ -1072,6 +1072,23 @@ class CefBrowserHost : public virtual CefBaseRefCounted {
|
||||
///
|
||||
/*--cef(default_retval=CEF_RUNTIME_STYLE_DEFAULT)--*/
|
||||
virtual cef_runtime_style_t GetRuntimeStyle() = 0;
|
||||
|
||||
#if CEF_API_ADDED(CEF_EXPERIMENTAL)
|
||||
///
|
||||
/// Enable or disable CDP accessibility tree viewport collapse for this
|
||||
/// browser. When enabled, off-screen landmarks and headings are serialized
|
||||
/// as summaries and other off-screen nodes are pruned. Overrides the
|
||||
/// CefBrowserSettings.ax_viewport_collapse value. If called on the UI thread
|
||||
/// the change will be applied immediately. Otherwise, the change will be
|
||||
/// applied asynchronously on the UI thread.
|
||||
/// WARNING: This collapses the CDP accessibility tree and disables CDP
|
||||
/// dynamic tree updates (nodesUpdated events). The DevTools Accessibility
|
||||
/// panel will show an incomplete tree. Platform screen readers (NVDA, JAWS,
|
||||
/// VoiceOver) are unaffected — they use a separate code path.
|
||||
///
|
||||
/*--cef(added=experimental)--*/
|
||||
virtual void SetAxViewportCollapse(bool enabled) = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // CEF_INCLUDE_CEF_BROWSER_H_
|
||||
@@ -721,6 +721,22 @@ typedef struct _cef_browser_settings_t {
|
||||
/// supported with Chrome style.
|
||||
///
|
||||
cef_state_t chrome_zoom_bubble;
|
||||
|
||||
#if CEF_API_ADDED(CEF_EXPERIMENTAL)
|
||||
///
|
||||
/// Controls whether CDP accessibility tree serialization collapses off-screen
|
||||
/// nodes. When enabled, off-screen landmarks and headings are serialized as
|
||||
/// summaries (role + name only) and other off-screen nodes are pruned.
|
||||
/// This reduces snapshot size for AI agents using Playwright ariaSnapshot().
|
||||
/// WARNING: This collapses the CDP accessibility tree and disables CDP
|
||||
/// dynamic tree updates (nodesUpdated events). The DevTools Accessibility
|
||||
/// panel will show an incomplete tree. Platform screen readers (NVDA, JAWS,
|
||||
/// VoiceOver) are unaffected — they use a separate code path.
|
||||
/// Can also be configured at runtime using
|
||||
/// CefBrowserHost::SetAxViewportCollapse.
|
||||
///
|
||||
cef_state_t ax_viewport_collapse;
|
||||
#endif
|
||||
} cef_browser_settings_t;
|
||||
|
||||
///
|
||||
|
||||
@@ -580,6 +580,9 @@ struct CefBrowserSettingsTraits {
|
||||
|
||||
target->chrome_status_bubble = src->chrome_status_bubble;
|
||||
target->chrome_zoom_bubble = src->chrome_zoom_bubble;
|
||||
#if CEF_API_ADDED(CEF_EXPERIMENTAL)
|
||||
target->ax_viewport_collapse = src->ax_viewport_collapse;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -976,6 +976,20 @@ cef_runtime_style_t CefBrowserHostBase::GetRuntimeStyle() {
|
||||
return IsAlloyStyle() ? CEF_RUNTIME_STYLE_ALLOY : CEF_RUNTIME_STYLE_CHROME;
|
||||
}
|
||||
|
||||
void CefBrowserHostBase::SetAxViewportCollapse(bool enabled) {
|
||||
if (!CEF_CURRENTLY_ON_UIT()) {
|
||||
CEF_POST_TASK(CEF_UIT,
|
||||
base::BindOnce(&CefBrowserHostBase::SetAxViewportCollapse,
|
||||
this, enabled));
|
||||
return;
|
||||
}
|
||||
|
||||
ax_viewport_collapse_ = enabled;
|
||||
if (auto* web_contents = GetWebContents()) {
|
||||
web_contents->OnWebPreferencesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void CefBrowserHostBase::ReplaceMisspelling(const CefString& word) {
|
||||
if (!CEF_CURRENTLY_ON_UIT()) {
|
||||
CEF_POST_TASK(
|
||||
|
||||
@@ -279,6 +279,7 @@ class CefBrowserHostBase : public CefBrowserHost,
|
||||
void ExitFullscreen(bool will_cause_resize) override;
|
||||
bool IsRenderProcessUnresponsive() override;
|
||||
cef_runtime_style_t GetRuntimeStyle() override;
|
||||
void SetAxViewportCollapse(bool enabled) override;
|
||||
|
||||
// CefBrowser methods:
|
||||
bool IsValid() override;
|
||||
@@ -385,6 +386,9 @@ class CefBrowserHostBase : public CefBrowserHost,
|
||||
}
|
||||
bool is_views_hosted() const { return is_views_hosted_; }
|
||||
SkColor GetBackgroundColor() const;
|
||||
std::optional<bool> ax_viewport_collapse() const {
|
||||
return ax_viewport_collapse_;
|
||||
}
|
||||
|
||||
// Returns true if windowless rendering is enabled.
|
||||
virtual bool IsWindowless() const = 0;
|
||||
@@ -518,6 +522,9 @@ class CefBrowserHostBase : public CefBrowserHost,
|
||||
};
|
||||
std::optional<AutoResizeConstraints> auto_resize_;
|
||||
|
||||
// Runtime override for CDP accessibility tree viewport collapse.
|
||||
std::optional<bool> ax_viewport_collapse_;
|
||||
|
||||
private:
|
||||
IMPLEMENT_REFCOUNTING(CefBrowserHostBase);
|
||||
};
|
||||
|
||||
@@ -426,6 +426,12 @@ void ChromeContentBrowserClientCef::OverrideWebPreferences(
|
||||
if (browser) {
|
||||
renderer_prefs::SetCefPrefs(browser->settings(), *prefs);
|
||||
|
||||
// Runtime override takes precedence over CefBrowserSettings.
|
||||
if (browser->ax_viewport_collapse().has_value()) {
|
||||
prefs->accessibility_viewport_collapse =
|
||||
browser->ax_viewport_collapse().value();
|
||||
}
|
||||
|
||||
// Set the background color for the WebView.
|
||||
base_background_color = browser->GetBackgroundColor();
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "cef/libcef/browser/prefs/renderer_prefs.h"
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "cef/libcef/common/api_version_util.h"
|
||||
#include "cef/libcef/common/cef_switches.h"
|
||||
#include "content/public/common/content_switches.h"
|
||||
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
|
||||
@@ -94,6 +95,10 @@ void SetCefPrefs(const CefBrowserSettings& cef,
|
||||
SET_STATE(cef.tab_to_links, web.tabs_to_links);
|
||||
SET_STATE(cef.local_storage, web.local_storage_enabled);
|
||||
|
||||
if (CEF_API_IS_ADDED(CEF_EXPERIMENTAL)) {
|
||||
SET_STATE(cef.ax_viewport_collapse, web.accessibility_viewport_collapse);
|
||||
}
|
||||
|
||||
// Never explicitly enable GPU-related functions in this method because the
|
||||
// GPU blacklist is not being checked here.
|
||||
if (cef.webgl == STATE_DISABLED) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 The Chromium Embedded Framework Authors. All rights
|
||||
// reserved. Use of this source code is governed by a BSD-style license that
|
||||
// can be found in the LICENSE file.
|
||||
|
||||
// Included from inspector_type_builder_helper.cc via patch.
|
||||
// Builds a minimal AXNode for off-screen landmarks and headings:
|
||||
// role, name, and heading level (if applicable). No children.
|
||||
|
||||
#include "ui/accessibility/ax_role_properties.h"
|
||||
|
||||
namespace blink {
|
||||
|
||||
std::unique_ptr<AXNode> BuildCollapsedSummaryNode(AXObject& ax_object) {
|
||||
auto node = AXNode::create()
|
||||
.setNodeId(String::Number(ax_object.AXObjectID()))
|
||||
.setIgnored(false)
|
||||
.build();
|
||||
|
||||
ax::mojom::blink::Role role = ax_object.RoleValue();
|
||||
node->setRole(CreateRoleNameValue(role));
|
||||
node->setChromeRole(CreateInternalRoleValue(role));
|
||||
|
||||
// Accessible name.
|
||||
ax::mojom::blink::NameFrom name_from;
|
||||
String computed_name = ax_object.GetName(name_from, nullptr, nullptr);
|
||||
node->setName(CreateValue(computed_name, AXValueTypeEnum::ComputedString));
|
||||
|
||||
// Heading level.
|
||||
if (ui::IsHeading(static_cast<ax::mojom::Role>(role))) {
|
||||
auto properties = std::make_unique<protocol::Array<AXProperty>>();
|
||||
int level = ax_object.HeadingLevel();
|
||||
if (level > 0) {
|
||||
properties->emplace_back(
|
||||
CreateProperty(AXPropertyNameEnum::Level, CreateValue(level)));
|
||||
}
|
||||
node->setProperties(std::move(properties));
|
||||
}
|
||||
|
||||
// Empty children.
|
||||
node->setChildIds(std::make_unique<protocol::Array<AXNodeId>>());
|
||||
|
||||
// DOM node reference.
|
||||
Node* dom_node = ax_object.GetNode();
|
||||
if (dom_node) {
|
||||
node->setBackendDOMNodeId(IdentifiersFactory::IntIdForNode(dom_node));
|
||||
}
|
||||
|
||||
// Parent reference.
|
||||
const AXObject* parent = ax_object.ParentObjectIncludedInTree();
|
||||
if (parent) {
|
||||
node->setParentId(String::Number(parent->AXObjectID()));
|
||||
} else if (ax_object.GetDocument() && ax_object.GetDocument()->GetFrame()) {
|
||||
auto& frame_token =
|
||||
ax_object.GetDocument()->GetFrame()->GetDevToolsFrameToken();
|
||||
node->setFrameId(IdentifiersFactory::IdFromToken(frame_token));
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
} // namespace blink
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2026 The Chromium Embedded Framework 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 "cef/libcef/renderer/accessibility/viewport_collapse.h"
|
||||
|
||||
#include "third_party/blink/renderer/core/dom/document.h"
|
||||
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
|
||||
#include "third_party/blink/renderer/core/frame/settings.h"
|
||||
#include "third_party/blink/renderer/core/frame/visual_viewport.h"
|
||||
#include "third_party/blink/renderer/core/page/page.h"
|
||||
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
|
||||
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
|
||||
#include "ui/accessibility/ax_role_properties.h"
|
||||
|
||||
namespace cef {
|
||||
|
||||
bool IsViewportCollapseEnabled(blink::Document& document) {
|
||||
auto* settings = document.GetSettings();
|
||||
return settings && settings->GetAccessibilityViewportCollapse();
|
||||
}
|
||||
|
||||
blink::PhysicalRect ComputeViewportRect(blink::Document& document) {
|
||||
blink::LocalFrameView* frame_view = document.View();
|
||||
|
||||
// The visual viewport's VisibleContentRect accounts for pinch-zoom scroll
|
||||
// but not layout viewport scroll. For a normal (non-zoomed) page, the
|
||||
// visual viewport offset is (0,0) and all scrolling is on the layout
|
||||
// viewport. We need the full visible area in document coordinates, so
|
||||
// combine both: start from the visual viewport rect (which has the correct
|
||||
// size and visual viewport offset) and add the layout viewport scroll.
|
||||
auto rect = blink::PhysicalRect(
|
||||
frame_view->GetPage()->GetVisualViewport().VisibleContentRect());
|
||||
if (auto* layout_viewport = frame_view->LayoutViewport()) {
|
||||
blink::ScrollOffset scroll = layout_viewport->GetScrollOffset();
|
||||
rect.offset.left += blink::LayoutUnit(scroll.x());
|
||||
rect.offset.top += blink::LayoutUnit(scroll.y());
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
bool IsNodeInViewport(const blink::AXObject& ax_object,
|
||||
const blink::PhysicalRect& viewport_rect) {
|
||||
blink::PhysicalRect bounds = ax_object.GetBoundsInFrameCoordinates();
|
||||
return bounds.Intersects(viewport_rect);
|
||||
}
|
||||
|
||||
ViewportCollapseAction ClassifyOffScreenNode(ax::mojom::blink::Role role) {
|
||||
ax::mojom::Role r = static_cast<ax::mojom::Role>(role);
|
||||
if (ui::IsLandmark(r) || ui::IsHeading(r)) {
|
||||
return ViewportCollapseAction::kSummary;
|
||||
}
|
||||
// Chromium maps <footer> to kFooter and <header> to kHeader, which are
|
||||
// distinct from kContentInfo/kBanner used by IsLandmark(). Treat them
|
||||
// as landmarks for collapsing purposes.
|
||||
if (r == ax::mojom::Role::kFooter || r == ax::mojom::Role::kHeader) {
|
||||
return ViewportCollapseAction::kSummary;
|
||||
}
|
||||
return ViewportCollapseAction::kPrune;
|
||||
}
|
||||
|
||||
} // namespace cef
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2026 The Chromium Embedded Framework Authors. All rights
|
||||
// reserved. Use of this source code is governed by a BSD-style license that
|
||||
// can be found in the LICENSE file.
|
||||
|
||||
#ifndef CEF_LIBCEF_RENDERER_ACCESSIBILITY_VIEWPORT_COLLAPSE_H_
|
||||
#define CEF_LIBCEF_RENDERER_ACCESSIBILITY_VIEWPORT_COLLAPSE_H_
|
||||
|
||||
#include "third_party/blink/renderer/core/layout/geometry/physical_rect.h"
|
||||
#include "ui/accessibility/ax_enums.mojom-blink.h"
|
||||
|
||||
namespace blink {
|
||||
class AXObject;
|
||||
class Document;
|
||||
} // namespace blink
|
||||
|
||||
namespace cef {
|
||||
|
||||
enum class ViewportCollapseAction { kSummary, kPrune };
|
||||
|
||||
// Check if viewport collapse is enabled for this document (reads Blink
|
||||
// Settings).
|
||||
bool IsViewportCollapseEnabled(blink::Document& document);
|
||||
|
||||
// Compute viewport rect from the document's visual viewport.
|
||||
blink::PhysicalRect ComputeViewportRect(blink::Document& document);
|
||||
|
||||
// Check if a node intersects the viewport.
|
||||
bool IsNodeInViewport(const blink::AXObject& ax_object,
|
||||
const blink::PhysicalRect& viewport_rect);
|
||||
|
||||
// Classify an off-screen node by role.
|
||||
ViewportCollapseAction ClassifyOffScreenNode(ax::mojom::blink::Role role);
|
||||
|
||||
} // namespace cef
|
||||
|
||||
#endif // CEF_LIBCEF_RENDERER_ACCESSIBILITY_VIEWPORT_COLLAPSE_H_
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright 2026 The Chromium Embedded Framework Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Included from inspector_accessibility_agent.cc via patch (anonymous
|
||||
// namespace). Viewport-collapse-aware variants of AddChildren and
|
||||
// WalkAXNodesToDepth.
|
||||
|
||||
// Check if a node might be in-viewport despite having an off-screen parent.
|
||||
// Only position:fixed and position:sticky elements can escape their parent's
|
||||
// off-screen bounds. This is much cheaper than GetBoundsInFrameCoordinates().
|
||||
bool MayBeVisibleDespiteOffscreenParent(const AXObject& ax_object) {
|
||||
auto* layout = ax_object.GetLayoutObject();
|
||||
return layout &&
|
||||
(layout->IsFixedPositioned() || layout->IsStickyPositioned());
|
||||
}
|
||||
|
||||
struct BfsEntry {
|
||||
AXID id;
|
||||
int depth;
|
||||
bool parent_offscreen;
|
||||
};
|
||||
|
||||
void AddChildrenWithCollapse(AXObject& ax_object,
|
||||
bool follow_ignored,
|
||||
std::unique_ptr<protocol::Array<AXNode>>& nodes,
|
||||
AXObjectCacheImpl& cache,
|
||||
const PhysicalRect& viewport_rect,
|
||||
HashSet<AXID>& serialized_ids,
|
||||
HashSet<AXID>& in_viewport_ids,
|
||||
bool parent_offscreen,
|
||||
Deque<BfsEntry>* queue,
|
||||
int depth,
|
||||
int max_depth) {
|
||||
// Track IDs of nodes reached via pruned off-screen parents. If such a
|
||||
// node turns out to be in-viewport (e.g. position:fixed), it must be
|
||||
// enqueued in the BFS since the normal child-enqueue loop won't find it.
|
||||
HashSet<AXID> from_pruned_ids;
|
||||
|
||||
HeapVector<Member<AXObject>> reachable;
|
||||
reachable.Append(ax_object.ChildrenIncludingIgnored().rbegin(),
|
||||
ax_object.ChildrenIncludingIgnored().rend());
|
||||
|
||||
while (!reachable.empty()) {
|
||||
AXObject* descendant = reachable.back();
|
||||
reachable.pop_back();
|
||||
if (descendant->IsDetached()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool from_pruned = from_pruned_ids.Contains(descendant->AXObjectID());
|
||||
|
||||
if (follow_ignored &&
|
||||
(descendant->IsIgnoredButIncludedInTree() || !descendant->GetNode())) {
|
||||
const auto& children = descendant->ChildrenIncludingIgnored();
|
||||
reachable.Append(children.rbegin(), children.rend());
|
||||
if (from_pruned) {
|
||||
for (const auto& child : children) {
|
||||
from_pruned_ids.insert(child->AXObjectID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine viewport status. If parent is off-screen, skip the expensive
|
||||
// bounds check unless this node has fixed/sticky positioning.
|
||||
bool effectively_offscreen = parent_offscreen || from_pruned;
|
||||
bool in_viewport;
|
||||
if (effectively_offscreen &&
|
||||
!MayBeVisibleDespiteOffscreenParent(*descendant)) {
|
||||
in_viewport = false;
|
||||
} else {
|
||||
in_viewport = cef::IsNodeInViewport(*descendant, viewport_rect);
|
||||
}
|
||||
|
||||
if (!in_viewport) {
|
||||
auto action = cef::ClassifyOffScreenNode(descendant->RoleValue());
|
||||
if (action == cef::ViewportCollapseAction::kSummary) {
|
||||
// Landmark/heading: emit summary, cut subtree entirely.
|
||||
nodes->emplace_back(BuildCollapsedSummaryNode(*descendant));
|
||||
serialized_ids.insert(descendant->AXObjectID());
|
||||
} else {
|
||||
// Pruned generic node: still visit children so that position:fixed
|
||||
// or position:sticky descendants can escape the off-screen parent.
|
||||
const auto& children = descendant->ChildrenIncludingIgnored();
|
||||
reachable.Append(children.rbegin(), children.rend());
|
||||
for (const auto& child : children) {
|
||||
from_pruned_ids.insert(child->AXObjectID());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
in_viewport_ids.insert(descendant->AXObjectID());
|
||||
auto child_node = BuildProtocolAXNodeForAXObject(*descendant);
|
||||
serialized_ids.insert(descendant->AXObjectID());
|
||||
nodes->emplace_back(std::move(child_node));
|
||||
|
||||
// If this in-viewport node was discovered via a pruned off-screen parent,
|
||||
// it won't be enqueued by the normal BFS child loop (which only checks
|
||||
// direct unignored children). Enqueue it here so its subtree is visited.
|
||||
if (from_pruned && queue && (max_depth == -1 || depth < max_depth)) {
|
||||
queue->emplace_back(BfsEntry{descendant->AXObjectID(), depth + 1, false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<protocol::Array<AXNode>> WalkAXNodesToDepthWithCollapse(
|
||||
AXObjectCacheImpl& cache,
|
||||
int max_depth) {
|
||||
auto nodes = std::make_unique<protocol::Array<AXNode>>();
|
||||
HashSet<AXID> serialized_ids;
|
||||
HashSet<AXID> in_viewport_ids; // Tracks which nodes passed bounds check
|
||||
PhysicalRect viewport_rect = cef::ComputeViewportRect(cache.GetDocument());
|
||||
|
||||
Deque<BfsEntry> queue;
|
||||
queue.emplace_back(BfsEntry{cache.Root()->AXObjectID(), 1, false});
|
||||
nodes->emplace_back(BuildProtocolAXNodeForAXObject(*cache.Root()));
|
||||
serialized_ids.insert(cache.Root()->AXObjectID());
|
||||
in_viewport_ids.insert(cache.Root()->AXObjectID());
|
||||
|
||||
while (!queue.empty()) {
|
||||
auto [id, depth, parent_offscreen] = queue.front();
|
||||
queue.pop_front();
|
||||
AXObject* ax_object = cache.ObjectFromAXID(id);
|
||||
if (!ax_object) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Serialize children — records viewport status in in_viewport_ids.
|
||||
// Passes queue so that in-viewport nodes found under pruned parents
|
||||
// can be enqueued for further traversal.
|
||||
AddChildrenWithCollapse(*ax_object, true, nodes, cache, viewport_rect,
|
||||
serialized_ids, in_viewport_ids, parent_offscreen,
|
||||
&queue, depth, max_depth);
|
||||
|
||||
// Enqueue in-viewport unignored children for depth traversal.
|
||||
// Uses in_viewport_ids instead of recomputing bounds.
|
||||
const auto& children = ax_object->UnignoredChildrenSlow();
|
||||
for (auto& child : children) {
|
||||
bool child_in_viewport = in_viewport_ids.Contains(child->AXObjectID());
|
||||
if (!child_in_viewport) {
|
||||
// Already handled (serialized as summary or pruned) in
|
||||
// AddChildrenWithCollapse. Don't recurse.
|
||||
continue;
|
||||
}
|
||||
if (max_depth == -1 || depth < max_depth) {
|
||||
queue.emplace_back(BfsEntry{child->AXObjectID(), depth + 1, false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter childIds to only reference serialized nodes.
|
||||
// Summary nodes already have empty childIds so this only matters for
|
||||
// full nodes whose children were partially pruned.
|
||||
for (auto& node : *nodes) {
|
||||
auto* child_ids = node->getChildIds(nullptr);
|
||||
if (!child_ids || child_ids->empty()) {
|
||||
continue;
|
||||
}
|
||||
auto filtered = std::make_unique<protocol::Array<AXNodeId>>();
|
||||
for (const auto& child_id : *child_ids) {
|
||||
if (serialized_ids.Contains(StringToIntStrict(child_id).value_or(0))) {
|
||||
filtered->emplace_back(child_id);
|
||||
}
|
||||
}
|
||||
node->setChildIds(std::move(filtered));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
@@ -850,5 +850,11 @@ patches = [
|
||||
# Support CEF component updater API.
|
||||
# https://github.com/chromiumembedded/cef/issues/4087
|
||||
'name': 'component_updater_4087'
|
||||
},
|
||||
{
|
||||
# Support per-browser accessibility viewport collapse for CDP tree collapsing.
|
||||
# Collapses off-screen nodes in CDP accessibility serialization to reduce
|
||||
# snapshot size for AI agents. Controlled via CefBrowserSettings.
|
||||
'name': 'blink_ax_viewport_collapse',
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,271 @@
|
||||
diff --git third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc
|
||||
index 23672617af41f1d88b551da7b4ad0a19e39a2092..6e9b90bad48d090a7f52b3598d0e57c7cbb66713 100644
|
||||
--- third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc
|
||||
+++ third_party/blink/common/web_preferences/web_preferences_mojom_traits.cc
|
||||
@@ -232,6 +232,7 @@ bool StructTraits<blink::mojom::WebPreferencesDataView,
|
||||
out->lazy_load_enabled = data.lazy_load_enabled();
|
||||
out->allow_mixed_content_upgrades = data.allow_mixed_content_upgrades();
|
||||
out->always_show_focus = data.always_show_focus();
|
||||
+ out->accessibility_viewport_collapse = data.accessibility_viewport_collapse();
|
||||
out->touch_drag_drop_enabled = data.touch_drag_drop_enabled();
|
||||
out->touch_dragend_context_menu = data.touch_dragend_context_menu();
|
||||
out->webxr_immersive_ar_allowed = data.webxr_immersive_ar_allowed();
|
||||
diff --git third_party/blink/public/common/web_preferences/web_preferences.h third_party/blink/public/common/web_preferences/web_preferences.h
|
||||
index cd34e8f534e86d4ff11ea1d8020d215f37cd730b..c2218516b5e09fae2629db13ca8f9f83d30f617a 100644
|
||||
--- third_party/blink/public/common/web_preferences/web_preferences.h
|
||||
+++ third_party/blink/public/common/web_preferences/web_preferences.h
|
||||
@@ -423,6 +423,9 @@ struct BLINK_COMMON_EXPORT WebPreferences {
|
||||
// forcing :focus-visible to match regardless of focus method).
|
||||
bool always_show_focus = false;
|
||||
|
||||
+ // Whether CDP accessibility tree serialization collapses off-screen nodes.
|
||||
+ bool accessibility_viewport_collapse = false;
|
||||
+
|
||||
// Whether touch input can trigger HTML drag-and-drop operations. The
|
||||
// default value depends on the platform.
|
||||
bool touch_drag_drop_enabled = false;
|
||||
diff --git third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h
|
||||
index 9bb06fb430cbf31b6fc1343229191565a62421a8..0656126a6ae01ce603cc56247b79a40f53dccc07 100644
|
||||
--- third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h
|
||||
+++ third_party/blink/public/common/web_preferences/web_preferences_mojom_traits.h
|
||||
@@ -785,6 +785,11 @@ struct BLINK_COMMON_EXPORT StructTraits<blink::mojom::WebPreferencesDataView,
|
||||
return r.always_show_focus;
|
||||
}
|
||||
|
||||
+ static bool accessibility_viewport_collapse(
|
||||
+ const blink::web_pref::WebPreferences& r) {
|
||||
+ return r.accessibility_viewport_collapse;
|
||||
+ }
|
||||
+
|
||||
static bool touch_drag_drop_enabled(
|
||||
const blink::web_pref::WebPreferences& r) {
|
||||
return r.touch_drag_drop_enabled;
|
||||
diff --git third_party/blink/public/mojom/webpreferences/web_preferences.mojom third_party/blink/public/mojom/webpreferences/web_preferences.mojom
|
||||
index ec6f13d3924cf861c505ed6d3abde0371fd65475..195d7a3a6c464c959239b55bd114f9c1a0b65131 100644
|
||||
--- third_party/blink/public/mojom/webpreferences/web_preferences.mojom
|
||||
+++ third_party/blink/public/mojom/webpreferences/web_preferences.mojom
|
||||
@@ -482,6 +482,9 @@ struct WebPreferences {
|
||||
// forcing :focus-visible to match regardless of focus method).
|
||||
bool always_show_focus;
|
||||
|
||||
+ // Whether CDP accessibility tree serialization collapses off-screen nodes.
|
||||
+ bool accessibility_viewport_collapse;
|
||||
+
|
||||
// Whether touch input can trigger HTML drag-and-drop operations. The
|
||||
// default value depends on the platform.
|
||||
bool touch_drag_drop_enabled;
|
||||
diff --git third_party/blink/public/web/web_settings.h third_party/blink/public/web/web_settings.h
|
||||
index 42dcdc73fd3255fc4af472ddcc885dd9444f9bae..d21caf62ce8f7175f9ffbd7432013b9b48680d57 100644
|
||||
--- third_party/blink/public/web/web_settings.h
|
||||
+++ third_party/blink/public/web/web_settings.h
|
||||
@@ -228,6 +228,7 @@ class WebSettings {
|
||||
virtual void SetAccessibilityFontScaleFactor(float) = 0;
|
||||
virtual void SetAccessibilityTextSizeContrastFactor(int) = 0;
|
||||
virtual void SetAccessibilityAlwaysShowFocus(bool) = 0;
|
||||
+ virtual void SetAccessibilityViewportCollapse(bool) = 0;
|
||||
virtual void SetTextTrackKindUserPreference(TextTrackKindUserPreference) = 0;
|
||||
virtual void SetTextTrackBackgroundColor(const WebString&) = 0;
|
||||
virtual void SetTextTrackFontFamily(const WebString&) = 0;
|
||||
diff --git third_party/blink/renderer/core/exported/web_settings_impl.cc third_party/blink/renderer/core/exported/web_settings_impl.cc
|
||||
index e3532e382addf2efde5e60d0f4ae985cac8232a0..3a6551e8aeaba43d4c91b94211b99e5e620dbf10 100644
|
||||
--- third_party/blink/renderer/core/exported/web_settings_impl.cc
|
||||
+++ third_party/blink/renderer/core/exported/web_settings_impl.cc
|
||||
@@ -175,6 +175,10 @@ void WebSettingsImpl::SetAccessibilityAlwaysShowFocus(bool always_show_focus) {
|
||||
settings_->SetAccessibilityAlwaysShowFocus(always_show_focus);
|
||||
}
|
||||
|
||||
+void WebSettingsImpl::SetAccessibilityViewportCollapse(bool enabled) {
|
||||
+ settings_->SetAccessibilityViewportCollapse(enabled);
|
||||
+}
|
||||
+
|
||||
void WebSettingsImpl::SetAccessibilityPasswordValuesEnabled(bool enabled) {
|
||||
settings_->SetAccessibilityPasswordValuesEnabled(enabled);
|
||||
}
|
||||
diff --git third_party/blink/renderer/core/exported/web_settings_impl.h third_party/blink/renderer/core/exported/web_settings_impl.h
|
||||
index 333634ea2567af0114a951ae47535ec42435cc9e..28bf3e044d69c8be0fed3d63f778bc551ce3797b 100644
|
||||
--- third_party/blink/renderer/core/exported/web_settings_impl.h
|
||||
+++ third_party/blink/renderer/core/exported/web_settings_impl.h
|
||||
@@ -174,6 +174,7 @@ class CORE_EXPORT WebSettingsImpl final : public WebSettings {
|
||||
void SetAccessibilityFontScaleFactor(float) override;
|
||||
void SetAccessibilityTextSizeContrastFactor(int) override;
|
||||
void SetAccessibilityAlwaysShowFocus(bool) override;
|
||||
+ void SetAccessibilityViewportCollapse(bool) override;
|
||||
void SetTextTrackKindUserPreference(TextTrackKindUserPreference) override;
|
||||
void SetTextTrackBackgroundColor(const WebString&) override;
|
||||
void SetTextTrackFontFamily(const WebString&) override;
|
||||
diff --git third_party/blink/renderer/core/exported/web_view_impl.cc third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
index f5bdcb8176a94705a3710ba5dbc536a96b35fed9..742ac4bcc69f02779535c376e2897f706a671eec 100644
|
||||
--- third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
+++ third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
@@ -1834,6 +1834,8 @@ void WebView::ApplyWebPreferences(const web_pref::WebPreferences& prefs,
|
||||
settings->SetForceDarkModeEnabled(prefs.force_dark_mode_enabled);
|
||||
|
||||
settings->SetAccessibilityAlwaysShowFocus(prefs.always_show_focus);
|
||||
+ settings->SetAccessibilityViewportCollapse(
|
||||
+ prefs.accessibility_viewport_collapse);
|
||||
settings->SetAutoplayPolicy(prefs.autoplay_policy);
|
||||
settings->SetRequireTransientActivationForGetDisplayMedia(
|
||||
prefs.require_transient_activation_for_get_display_media);
|
||||
@@ -3250,6 +3252,7 @@ void WebViewImpl::OnWindowShowStateChanged(
|
||||
WasRestored();
|
||||
}
|
||||
break;
|
||||
+ case WindowShowState::kHidden:
|
||||
case WindowShowState::kInactive:
|
||||
case WindowShowState::kFullscreen:
|
||||
case WindowShowState::kEnd:
|
||||
diff --git third_party/blink/renderer/core/frame/settings.json5 third_party/blink/renderer/core/frame/settings.json5
|
||||
index d9d3a09b53e250b7a019195214186add0b85d562..9de56e24eca711dc82700ad3f49ed92a32ee4c75 100644
|
||||
--- third_party/blink/renderer/core/frame/settings.json5
|
||||
+++ third_party/blink/renderer/core/frame/settings.json5
|
||||
@@ -1231,6 +1231,10 @@
|
||||
name: "accessibilityIncludeSvgGElement",
|
||||
initial: false,
|
||||
},
|
||||
+ {
|
||||
+ name: "accessibilityViewportCollapse",
|
||||
+ initial: false,
|
||||
+ },
|
||||
{
|
||||
name: "bypassCSP",
|
||||
initial: false,
|
||||
diff --git third_party/blink/renderer/modules/accessibility/BUILD.gn third_party/blink/renderer/modules/accessibility/BUILD.gn
|
||||
index f9de92e534a5eaaa012633b72e593a3075bbf48c..50bd9bac40ab76c1eda9eb3bc964ca41a087918f 100644
|
||||
--- third_party/blink/renderer/modules/accessibility/BUILD.gn
|
||||
+++ third_party/blink/renderer/modules/accessibility/BUILD.gn
|
||||
@@ -2,6 +2,7 @@
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
+import("//cef/libcef/features/features.gni")
|
||||
import("//third_party/blink/renderer/modules/modules.gni")
|
||||
|
||||
blink_modules_sources("accessibility") {
|
||||
@@ -74,6 +75,17 @@ blink_modules_sources("accessibility") {
|
||||
]
|
||||
}
|
||||
|
||||
+ if (enable_cef) {
|
||||
+ sources += [
|
||||
+ "//cef/libcef/renderer/accessibility/build_collapsed_summary_node.inc",
|
||||
+ "//cef/libcef/renderer/accessibility/viewport_collapse.cc",
|
||||
+ "//cef/libcef/renderer/accessibility/viewport_collapse.h",
|
||||
+ "//cef/libcef/renderer/accessibility/walk_ax_nodes_with_collapse.inc",
|
||||
+ ]
|
||||
+ configs += [ "//cef/libcef/features:config" ]
|
||||
+ deps += [ "//cef/libcef/features:deps" ]
|
||||
+ }
|
||||
+
|
||||
if (is_android) {
|
||||
defines = [ "REDUCE_AX_INLINE_TEXTBOXES" ]
|
||||
}
|
||||
diff --git third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
|
||||
index e7f9651c0865b36aecf96a44c1061a7a40aabf43..91f0feb8e2f31c53f9425b5d874b6dc71d0544c1 100644
|
||||
--- third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
|
||||
+++ third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.cc
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "base/containers/adapters.h"
|
||||
+#include "cef/libcef/features/features.h"
|
||||
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
|
||||
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
|
||||
#include "third_party/blink/renderer/core/dom/element.h"
|
||||
@@ -30,6 +31,10 @@
|
||||
#include "ui/accessibility/ax_mode.h"
|
||||
#include "ui/accessibility/ax_node_data.h"
|
||||
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+#include "cef/libcef/renderer/accessibility/viewport_collapse.h"
|
||||
+#endif
|
||||
+
|
||||
namespace blink {
|
||||
|
||||
namespace {
|
||||
@@ -132,6 +137,10 @@ std::unique_ptr<protocol::Array<AXNode>> WalkAXNodesToDepth(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+#include "cef/libcef/renderer/accessibility/walk_ax_nodes_with_collapse.inc"
|
||||
+#endif
|
||||
+
|
||||
} // namespace
|
||||
|
||||
using EnabledAgentsMultimap =
|
||||
@@ -245,7 +254,14 @@ protocol::Response InspectorAccessibilityAgent::getFullAXTree(
|
||||
cache.UpdateAXForAllDocuments();
|
||||
ScopedFreezeAXCache freeze(cache);
|
||||
|
||||
- *nodes = WalkAXNodesToDepth(cache, depth.value_or(-1));
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+ if (cef::IsViewportCollapseEnabled(cache.GetDocument())) {
|
||||
+ *nodes = WalkAXNodesToDepthWithCollapse(cache, depth.value_or(-1));
|
||||
+ } else
|
||||
+#endif
|
||||
+ {
|
||||
+ *nodes = WalkAXNodesToDepth(cache, depth.value_or(-1));
|
||||
+ }
|
||||
|
||||
return protocol::Response::Success();
|
||||
}
|
||||
@@ -509,6 +525,15 @@ void InspectorAccessibilityAgent::AXReadyCallback(Document& document) {
|
||||
}
|
||||
|
||||
void InspectorAccessibilityAgent::ProcessPendingDirtyNodes(Document& document) {
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+ // Suppress nodesUpdated events when viewport collapse is active.
|
||||
+ // These events send uncollapsed data, creating an inconsistent tree.
|
||||
+ // AI agents call getFullAXTree() directly without enable(), so they
|
||||
+ // never receive these events anyway.
|
||||
+ if (cef::IsViewportCollapseEnabled(document))
|
||||
+ return;
|
||||
+#endif
|
||||
+
|
||||
auto now = base::Time::Now();
|
||||
|
||||
if (!last_sync_times_.Contains(&document))
|
||||
diff --git third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
|
||||
index ad0547c3c362ea975bca43b53592d88afbd15603..6dac1c010b2b3a694d1c0c87b335f2009fa30079 100644
|
||||
--- third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
|
||||
+++ third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.cc
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h"
|
||||
|
||||
+#include "cef/libcef/features/features.h"
|
||||
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
|
||||
#include "third_party/blink/renderer/core/frame/local_frame.h"
|
||||
#include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
|
||||
@@ -922,3 +923,7 @@ std::unique_ptr<AXNode> BuildProtocolAXNodeForUnignoredAXObject(
|
||||
}
|
||||
|
||||
} // namespace blink
|
||||
+
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+#include "cef/libcef/renderer/accessibility/build_collapsed_summary_node.inc"
|
||||
+#endif
|
||||
diff --git third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
|
||||
index 2c8d5a8aada7230bc141feed9e1af2e6e95d3664..ee1829e828b30eb33e9f4834db7ed3f81280862c 100644
|
||||
--- third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
|
||||
+++ third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "third_party/blink/renderer/core/accessibility/axid.h"
|
||||
#include "third_party/blink/renderer/core/inspector/protocol/accessibility.h"
|
||||
|
||||
+#include "cef/libcef/features/features.h"
|
||||
+
|
||||
namespace blink {
|
||||
|
||||
class AXObject;
|
||||
@@ -25,6 +27,10 @@ std::unique_ptr<AXNode> BuildProtocolAXNodeForIgnoredAXObject(
|
||||
bool force_name_and_role);
|
||||
std::unique_ptr<AXNode> BuildProtocolAXNodeForUnignoredAXObject(AXObject&);
|
||||
|
||||
+#if BUILDFLAG(ENABLE_CEF)
|
||||
+std::unique_ptr<AXNode> BuildCollapsedSummaryNode(AXObject& ax_object);
|
||||
+#endif
|
||||
+
|
||||
} // namespace blink
|
||||
|
||||
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_ACCESSIBILITY_INSPECTOR_TYPE_BUILDER_HELPER_H_
|
||||
@@ -214,18 +214,6 @@ index c87f079a6541866fa00cc96e7dfed11d743ba182..17c949f873124021ae69b2fe9d469913
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
// Set the view's active state (i.e., tint state of controls).
|
||||
virtual void SetActive(bool active) = 0;
|
||||
diff --git third_party/blink/renderer/core/exported/web_view_impl.cc third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
index f5bdcb8176a94705a3710ba5dbc536a96b35fed9..09122a6fdc4dbdc61fb580110473265c6ee2c661 100644
|
||||
--- third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
+++ third_party/blink/renderer/core/exported/web_view_impl.cc
|
||||
@@ -3250,6 +3250,7 @@ void WebViewImpl::OnWindowShowStateChanged(
|
||||
WasRestored();
|
||||
}
|
||||
break;
|
||||
+ case WindowShowState::kHidden:
|
||||
case WindowShowState::kInactive:
|
||||
case WindowShowState::kFullscreen:
|
||||
case WindowShowState::kEnd:
|
||||
diff --git ui/aura/native_window_occlusion_tracker_win.cc ui/aura/native_window_occlusion_tracker_win.cc
|
||||
index 883f23810672adf94c8b86f8ec54dc6ac9e7acbb..5d1a2b11c2cea3b247077f96c35780e40efa70f5 100644
|
||||
--- ui/aura/native_window_occlusion_tracker_win.cc
|
||||
|
||||
@@ -241,6 +241,12 @@ void MainContextImpl::PopulateBrowserSettings(CefBrowserSettings* settings) {
|
||||
settings->chrome_status_bubble = STATE_DISABLED;
|
||||
settings->chrome_zoom_bubble = STATE_DISABLED;
|
||||
}
|
||||
|
||||
#if CEF_API_ADDED(CEF_EXPERIMENTAL)
|
||||
if (command_line_->HasSwitch(switches::kAxViewportCollapse)) {
|
||||
settings->ax_viewport_collapse = STATE_ENABLED;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainContextImpl::PopulateOsrSettings(OsrRendererSettings* settings) {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -65,5 +65,6 @@ const char kUseChromeStyleWindow[] = "use-chrome-style-window";
|
||||
const char kShowOverlayBrowser[] = "show-overlay-browser";
|
||||
const char kUseAngle[] = "use-angle";
|
||||
const char kOzonePlatform[] = "ozone-platform";
|
||||
const char kAxViewportCollapse[] = "ax-viewport-collapse";
|
||||
|
||||
} // namespace client::switches
|
||||
@@ -59,6 +59,7 @@ extern const char kUseChromeStyleWindow[];
|
||||
extern const char kShowOverlayBrowser[];
|
||||
extern const char kUseAngle[];
|
||||
extern const char kOzonePlatform[];
|
||||
extern const char kAxViewportCollapse[];
|
||||
|
||||
} // namespace client::switches
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# ax_viewport_collapse Debug Tools
|
||||
|
||||
Manual verification tools for the `ax_viewport_collapse` CefBrowserSettings feature.
|
||||
These connect to a running cefclient via CDP to inspect the accessibility tree.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```
|
||||
python3 -m pip install websocket-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Launch cefclient with remote debugging
|
||||
|
||||
```
|
||||
cefclient.exe --remote-debugging-port=9222 --remote-allow-origins=* --ax-viewport-collapse
|
||||
```
|
||||
|
||||
The `--ax-viewport-collapse` flag maps to `CefBrowserSettings.ax_viewport_collapse = STATE_ENABLED` in cefclient.
|
||||
|
||||
To load the test page directly:
|
||||
|
||||
```
|
||||
cefclient.exe --remote-debugging-port=9222 --remote-allow-origins=* --ax-viewport-collapse --url=file:///<path>/test_viewport_collapse.html
|
||||
```
|
||||
|
||||
### 2. Run the verification script
|
||||
|
||||
```
|
||||
python3 verify_viewport_collapse.py
|
||||
```
|
||||
|
||||
Or navigate to a URL first:
|
||||
|
||||
```
|
||||
python3 verify_viewport_collapse.py --url https://example.com
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--host` | CDP host (default: `127.0.0.1`) |
|
||||
| `--port` | CDP port (default: `9222`) |
|
||||
| `--url` | Navigate to URL before fetching tree |
|
||||
| `--raw` | Print full JSON response instead of summary |
|
||||
|
||||
### Comparing with/without collapse
|
||||
|
||||
Run two cefclient instances on different ports:
|
||||
|
||||
```
|
||||
cefclient.exe --cache-path=<path1> --remote-debugging-port=9222 --remote-allow-origins=* --ax-viewport-collapse
|
||||
cefclient.exe --cache-path=<path2> --remote-debugging-port=9223 --remote-allow-origins=*
|
||||
```
|
||||
|
||||
```
|
||||
python3 verify_viewport_collapse.py --port 9222 --raw > with.json
|
||||
python3 verify_viewport_collapse.py --port 9223 --raw > without.json
|
||||
diff with.json without.json
|
||||
```
|
||||
|
||||
### Expected output (with collapse enabled)
|
||||
|
||||
```
|
||||
AX Tree Summary
|
||||
============================================================
|
||||
Total nodes returned: 77
|
||||
Ignored: 3
|
||||
Full (with children):70
|
||||
Summary (collapsed): 4
|
||||
|
||||
Collapsed summary nodes (off-screen landmarks/headings):
|
||||
------------------------------------------------------------
|
||||
region: "Content preferences"
|
||||
region: "Audio quality"
|
||||
region: "Display"
|
||||
region: "Storage"
|
||||
```
|
||||
|
||||
Off-screen landmark sections appear as leaf nodes with role + name only. In-viewport nodes are fully serialized with children.
|
||||
|
||||
## Files
|
||||
|
||||
- `verify_viewport_collapse.py` — CDP script that calls `Accessibility.getFullAXTree()` and prints a summary or raw JSON
|
||||
- `test_viewport_collapse.html` — Test page with 6 landmark sections, 2 in viewport and 4 pushed off-screen by a spacer
|
||||
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Viewport Collapse Test Page</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; padding: 20px; }
|
||||
section { margin: 20px 0; padding: 20px; border: 1px solid #ccc; border-radius: 8px; }
|
||||
h2 { margin-top: 0; }
|
||||
.spacer { height: 200vh; } /* Push later sections well off-screen */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header role="banner">
|
||||
<h1>Viewport Collapse Test</h1>
|
||||
<nav role="navigation" aria-label="Main navigation">
|
||||
<a href="#account">Account</a> |
|
||||
<a href="#language">Language</a> |
|
||||
<a href="#content">Content</a> |
|
||||
<a href="#audio">Audio Quality</a> |
|
||||
<a href="#display">Display</a> |
|
||||
<a href="#storage">Storage</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
|
||||
<!-- These two sections should be IN the viewport -->
|
||||
<section id="account" role="region" aria-label="Account">
|
||||
<h2>Account</h2>
|
||||
<p>Manage your account settings.</p>
|
||||
<button>Edit login methods</button>
|
||||
<button>Change password</button>
|
||||
<label>Email: <input type="text" value="user@example.com"></label>
|
||||
</section>
|
||||
|
||||
<section id="language" role="region" aria-label="Language">
|
||||
<h2>Language</h2>
|
||||
<label>Choose language:
|
||||
<select>
|
||||
<option selected>English</option>
|
||||
<option>Spanish</option>
|
||||
<option>French</option>
|
||||
<option>German</option>
|
||||
<option>Japanese</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<!-- Spacer to push remaining sections off-screen -->
|
||||
<div class="spacer" aria-hidden="true"></div>
|
||||
|
||||
<!-- These sections should be OFF-screen and collapsed -->
|
||||
<section id="content" role="region" aria-label="Content preferences">
|
||||
<h2>Content preferences</h2>
|
||||
<label><input type="checkbox" checked> Allow explicit content</label>
|
||||
<p>When off, explicit music won't be played.</p>
|
||||
</section>
|
||||
|
||||
<section id="audio" role="region" aria-label="Audio quality">
|
||||
<h2>Audio quality</h2>
|
||||
<label>Streaming quality:
|
||||
<select>
|
||||
<option>Automatic</option>
|
||||
<option selected>High</option>
|
||||
<option>Very high</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Download quality:
|
||||
<select>
|
||||
<option selected>Normal</option>
|
||||
<option>High</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><input type="checkbox"> Auto adjust quality</label>
|
||||
</section>
|
||||
|
||||
<section id="display" role="region" aria-label="Display">
|
||||
<h2>Display</h2>
|
||||
<label><input type="checkbox"> Use compact library layout</label>
|
||||
<label><input type="checkbox" checked> Show album art</label>
|
||||
<button>Reset display settings</button>
|
||||
</section>
|
||||
|
||||
<section id="storage" role="region" aria-label="Storage">
|
||||
<h2>Storage</h2>
|
||||
<p>Cache size: 2.4 GB</p>
|
||||
<button>Clear cache</button>
|
||||
<button>Manage downloads</button>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer role="contentinfo">
|
||||
<p>© 2026 Test App. All rights reserved.</p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Verify --ax-viewport-collapse behavior via raw CDP.
|
||||
|
||||
Usage:
|
||||
1. Launch cefclient with:
|
||||
out\\Debug_GN_x64\\cefclient.exe --remote-debugging-port=9222 --ax-viewport-collapse
|
||||
|
||||
2. Navigate to a page with scrollable content, e.g. the test page:
|
||||
out\\Debug_GN_x64\\cefclient.exe --remote-debugging-port=9222 --ax-viewport-collapse \\
|
||||
--url=file:///path/to/cef/tools/debug/ax_viewport_collapse/test_viewport_collapse.html
|
||||
|
||||
3. Run this script:
|
||||
python cef/tools/debug/ax_viewport_collapse/verify_viewport_collapse.py [--port 9222]
|
||||
|
||||
The script connects via CDP, optionally navigates, calls Accessibility.getFullAXTree(),
|
||||
and prints a summary showing which nodes were serialized as full, summary, or absent.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import http.client
|
||||
import websocket # pip install websocket-client
|
||||
|
||||
|
||||
def get_ws_url(host, port):
|
||||
"""Get the first available CDP target's websocket URL."""
|
||||
conn = http.client.HTTPConnection(host, port)
|
||||
conn.request("GET", "/json")
|
||||
resp = conn.getresponse()
|
||||
targets = json.loads(resp.read())
|
||||
conn.close()
|
||||
for t in targets:
|
||||
if t.get("type") == "page" and "webSocketDebuggerUrl" in t:
|
||||
return t["webSocketDebuggerUrl"]
|
||||
raise RuntimeError(
|
||||
f"No page target found on {host}:{port}. Targets: {json.dumps(targets, indent=2)}"
|
||||
)
|
||||
|
||||
|
||||
def cdp_send(ws, method, params=None, id_counter=[0]):
|
||||
id_counter[0] += 1
|
||||
msg = {"id": id_counter[0], "method": method, "params": params or {}}
|
||||
ws.send(json.dumps(msg))
|
||||
while True:
|
||||
resp = json.loads(ws.recv())
|
||||
if resp.get("id") == id_counter[0]:
|
||||
if "error" in resp:
|
||||
raise RuntimeError(f"CDP error: {resp['error']}")
|
||||
return resp.get("result", {})
|
||||
|
||||
|
||||
def navigate_and_wait(ws, url):
|
||||
"""Navigate to URL and wait for load."""
|
||||
cdp_send(ws, "Page.enable")
|
||||
cdp_send(ws, "Page.navigate", {"url": url})
|
||||
# Wait for loadEventFired
|
||||
while True:
|
||||
resp = json.loads(ws.recv())
|
||||
if resp.get("method") == "Page.loadEventFired":
|
||||
break
|
||||
|
||||
|
||||
LANDMARK_ROLES = {
|
||||
"banner", "complementary", "contentinfo", "form", "main", "navigation",
|
||||
"region", "search"
|
||||
}
|
||||
HEADING_ROLES = {"heading"}
|
||||
INTERACTIVE_ROLES = {
|
||||
"button", "checkbox", "link", "textbox", "combobox", "listbox", "menuitem",
|
||||
"radio", "slider", "switch", "tab", "searchbox", "spinbutton", "tree"
|
||||
}
|
||||
|
||||
|
||||
def classify_role(role_value):
|
||||
"""Classify a CDP role string."""
|
||||
r = (role_value or "").lower()
|
||||
if r in LANDMARK_ROLES:
|
||||
return "landmark"
|
||||
if r in HEADING_ROLES:
|
||||
return "heading"
|
||||
if r in INTERACTIVE_ROLES:
|
||||
return "interactive"
|
||||
return "structural"
|
||||
|
||||
|
||||
def analyze_tree(nodes):
|
||||
"""Analyze the AX tree and print a summary."""
|
||||
total = len(nodes)
|
||||
ignored = 0
|
||||
full_nodes = []
|
||||
summary_nodes = [] # Has role+name but empty children
|
||||
|
||||
for node in nodes:
|
||||
if node.get("ignored"):
|
||||
ignored += 1
|
||||
continue
|
||||
|
||||
child_ids = node.get("childIds", [])
|
||||
role_obj = node.get("role", {})
|
||||
role_value = role_obj.get("value", "") if isinstance(role_obj, dict) else ""
|
||||
name_obj = node.get("name", {})
|
||||
name_value = name_obj.get("value", "") if isinstance(name_obj, dict) else ""
|
||||
properties = node.get("properties", [])
|
||||
|
||||
has_name = bool(name_value)
|
||||
has_children = len(child_ids) > 0
|
||||
has_full_props = len(properties) > 2 # More than just level
|
||||
|
||||
cat = classify_role(role_value)
|
||||
|
||||
if cat in ("landmark", "heading") and has_name and not has_children:
|
||||
summary_nodes.append(node)
|
||||
else:
|
||||
full_nodes.append(node)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"AX Tree Summary")
|
||||
print(f"{'='*60}")
|
||||
print(f"Total nodes returned: {total}")
|
||||
print(f" Ignored: {ignored}")
|
||||
print(f" Full (with children):{len(full_nodes)}")
|
||||
print(f" Summary (collapsed): {len(summary_nodes)}")
|
||||
print()
|
||||
|
||||
if summary_nodes:
|
||||
print("Collapsed summary nodes (off-screen landmarks/headings):")
|
||||
print("-" * 60)
|
||||
for node in summary_nodes:
|
||||
role_obj = node.get("role", {})
|
||||
role_val = role_obj.get("value", "?") if isinstance(role_obj,
|
||||
dict) else "?"
|
||||
name_obj = node.get("name", {})
|
||||
name_val = name_obj.get("value", "") if isinstance(name_obj, dict) else ""
|
||||
props = node.get("properties", [])
|
||||
level_str = ""
|
||||
for p in props:
|
||||
if isinstance(p, dict) and p.get("name") == "level":
|
||||
lv = p.get("value", {})
|
||||
level_str = f" [level={lv.get('value', '?')}]"
|
||||
print(f" {role_val}: \"{name_val}\"{level_str}")
|
||||
print()
|
||||
|
||||
# Print the tree structure (first 50 non-ignored nodes)
|
||||
print("First 50 non-ignored nodes:")
|
||||
print("-" * 60)
|
||||
node_map = {n.get("nodeId"): n for n in nodes}
|
||||
count = 0
|
||||
for node in nodes:
|
||||
if node.get("ignored"):
|
||||
continue
|
||||
if count >= 50:
|
||||
print(" ... (truncated)")
|
||||
break
|
||||
count += 1
|
||||
|
||||
role_obj = node.get("role", {})
|
||||
role_val = role_obj.get("value", "?") if isinstance(role_obj, dict) else "?"
|
||||
name_obj = node.get("name", {})
|
||||
name_val = name_obj.get("value", "") if isinstance(name_obj, dict) else ""
|
||||
child_ids = node.get("childIds", [])
|
||||
|
||||
name_str = f" \"{name_val}\"" if name_val else ""
|
||||
children_str = f" [{len(child_ids)} children]" if child_ids else " [leaf]"
|
||||
is_summary = node in summary_nodes
|
||||
tag = " <-- COLLAPSED" if is_summary else ""
|
||||
|
||||
print(f" {role_val}{name_str}{children_str}{tag}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Verify CDP accessibility viewport collapse")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=9222)
|
||||
parser.add_argument("--url", help="URL to navigate to before fetching tree")
|
||||
parser.add_argument(
|
||||
"--raw", action="store_true", help="Print raw JSON response")
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"Connecting to CDP at {args.host}:{args.port}...")
|
||||
ws_url = get_ws_url(args.host, args.port)
|
||||
print(f"WebSocket: {ws_url}")
|
||||
|
||||
ws = websocket.create_connection(ws_url)
|
||||
|
||||
if args.url:
|
||||
print(f"Navigating to {args.url}...")
|
||||
navigate_and_wait(ws, args.url)
|
||||
print("Page loaded.")
|
||||
|
||||
print("Enabling accessibility domain...")
|
||||
cdp_send(ws, "Accessibility.enable")
|
||||
|
||||
print("Fetching full AX tree...")
|
||||
result = cdp_send(ws, "Accessibility.getFullAXTree")
|
||||
nodes = result.get("nodes", [])
|
||||
|
||||
if args.raw:
|
||||
print(json.dumps(nodes, indent=2))
|
||||
else:
|
||||
analyze_tree(nodes)
|
||||
|
||||
ws.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-1
@@ -12,7 +12,7 @@ from git_util import get_changed_files
|
||||
from yapf_util import yapf_format
|
||||
|
||||
# File extensions that can be formatted.
|
||||
DEFAULT_LINT_WHITELIST_REGEX = r"(.*\.cpp|.*\.cc|.*\.c|.*\.h|.*\.java|.*\.mm|.*\.m|.*\.py)$"
|
||||
DEFAULT_LINT_WHITELIST_REGEX = r"(.*\.cpp|.*\.cc|.*\.c|.*\.h|.*\.inc|.*\.java|.*\.mm|.*\.m|.*\.py)$"
|
||||
DEFAULT_LINT_BLACKLIST_REGEX = r"$^"
|
||||
|
||||
# Directories containing these path components will be ignored.
|
||||
|
||||
Reference in new issue
Block a user