Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.17.2...main) ##

### Added
- `page.accessibility` API and `Node#axnode` for reading the CDP accessibility tree

### Changed

Expand Down
96 changes: 96 additions & 0 deletions lib/ferrum/accessibility.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true

require "ferrum/accessibility/ax_node"

module Ferrum
#
# Wraps the CDP [Accessibility](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/)
# domain. The query commands work without `enable`; `enable`/`disable` are
# provided for completeness (live AX events).
#
class Accessibility
def initialize(page)
@page = page
end

#
# The single non-ignored AXNode for a DOM node, or `nil`.
#
# @param [Ferrum::Node] node
# @return [AXNode, nil]
#
def node_for(node)
partial_tree(node: node).find { |ax_node| !ax_node.ignored? }
end

#
# The partial AX tree for a DOM node.
#
# @param [Ferrum::Node] node
# @param [Boolean] fetch_relatives
# @return [Array<AXNode>]
#
def partial_tree(node:, fetch_relatives: false)
nodes = node.page.command("Accessibility.getPartialAXTree",
nodeId: node.node_id,
fetchRelatives: fetch_relatives)["nodes"]
build(nodes)
end

#
# The full AX tree for the page.
#
# @param [Integer, nil] depth
# @param [String, nil] frame_id
# @return [Array<AXNode>]
#
def snapshot(depth: nil, frame_id: nil)
params = { depth: depth, frameId: frame_id }.compact
build(@page.command("Accessibility.getFullAXTree", **params)["nodes"])
end

#
# Query the AX tree by accessible name and/or role.
#
# @param [String, nil] name
# @param [String, nil] role
# @param [Ferrum::Node, nil] node Scope the query to this node's subtree.
# @return [Array<AXNode>]
#
def query(name: nil, role: nil, node: nil)
page = node ? node.page : @page
params = { accessibleName: name, role: role }.compact
params[:nodeId] = node ? node.node_id : @page.document_node_id
build(page.command("Accessibility.queryAXTree", **params)["nodes"])
end

#
# The root AXNode of the (optionally framed) document.
#
# @param [String, nil] frame_id
# @return [AXNode, nil]
#
def root(frame_id: nil)
params = { depth: 1, frameId: frame_id }.compact
build(@page.command("Accessibility.getFullAXTree", **params)["nodes"]).first
end

# @return [self]
def enable
@page.command("Accessibility.enable")
self
end

# @return [self]
def disable
@page.command("Accessibility.disable")
self
end

private

def build(nodes)
Array(nodes).map { |node| AXNode.new(node) }
end
end
end
88 changes: 88 additions & 0 deletions lib/ferrum/accessibility/ax_node.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# frozen_string_literal: true

module Ferrum
class Accessibility
#
# Represents an [AXNode](https://chromedevtools.github.io/devtools-protocol/tot/Accessibility/#type-AXNode)
# from the CDP Accessibility domain.
#
class AXNode
#
# @param [Hash{String => Object}] params
# The parsed CDP AXNode attributes.
#
def initialize(params)
@params = deep_freeze(params)
end

# @return [String, nil]
def role
@params.dig("role", "value")
end

# @return [String, nil]
def name
@params.dig("name", "value")
end

# @return [String, nil]
def description
@params.dig("description", "value")
end

# @return [String, Numeric, Boolean, nil] raw CDP AXValue.value; type varies by control
def value
@params.dig("value", "value")
end

# @return [Hash{String => Object}]
# ARIA/computed properties flattened to `name => value`.
def properties
Array(@params["properties"]).to_h do |property|
[property["name"], property.dig("value", "value")]
end
end

# @return [Boolean]
def ignored?
@params["ignored"] == true
end

# @return [Array, nil]
def ignored_reasons
@params["ignoredReasons"]
end

# @return [String, nil]
def node_id
@params["nodeId"]
end

# @return [Integer, nil]
def backend_dom_node_id
@params["backendDOMNodeId"]
end

# @return [Array, nil]
def child_ids
@params["childIds"]
end

# @return [Hash]
# The raw CDP AXNode hash.
def to_h
@params
end

private

def deep_freeze(object)
case object
when Hash then object.each_value { |value| deep_freeze(value) }
when Array then object.each { |value| deep_freeze(value) }
end
object.freeze
end
end
end
end
2 changes: 1 addition & 1 deletion lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Browser
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
at_css at_xpath css xpath current_url current_title url title
body doctype content=
headers cookies network downloads
headers cookies network accessibility downloads
mouse keyboard
screenshot pdf mhtml viewport_size device_pixel_ratio
start_screencast stop_screencast
Expand Down
8 changes: 8 additions & 0 deletions lib/ferrum/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ def computed_style
.each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
end

# Returns the computed accessibility node for the element, or nil if the
# element is ignored by the accessibility tree.
#
# @return [Accessibility::AXNode, nil]
def axnode
page.accessibility.node_for(self)
end

def remove
page.command("DOM.removeNode", nodeId: node_id)
end
Expand Down
7 changes: 7 additions & 0 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "ferrum/cookies"
require "ferrum/dialog"
require "ferrum/network"
require "ferrum/accessibility"
require "ferrum/downloads"
require "ferrum/page/frames"
require "ferrum/page/screencast"
Expand Down Expand Up @@ -57,6 +58,11 @@ class Page
# @return [Network]
attr_reader :network

# Accessibility object.
#
# @return [Accessibility]
attr_reader :accessibility

# Headers object.
#
# @return [Headers]
Expand Down Expand Up @@ -88,6 +94,7 @@ def initialize(client, context_id:, target_id:, proxy: nil)
@headers = Headers.new(self)
@cookies = Cookies.new(self)
@network = Network.new(self)
@accessibility = Accessibility.new(self)
@tracing = Tracing.new(self)
@downloads = Downloads.new(self)

Expand Down
25 changes: 25 additions & 0 deletions sig/ferrum/accessibility.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Ferrum
class Accessibility
@page: Page

def initialize: (Page page) -> void

def node_for: (Node node) -> AXNode?

def partial_tree: (node: Node, ?fetch_relatives: bool) -> Array[AXNode]

def snapshot: (?depth: Integer?, ?frame_id: String?) -> Array[AXNode]

def query: (?name: String?, ?role: String?, ?node: Node?) -> Array[AXNode]

def root: (?frame_id: String?) -> AXNode?

def enable: () -> self

def disable: () -> self

private

def build: (Array[untyped]? nodes) -> Array[AXNode]
end
end
31 changes: 31 additions & 0 deletions sig/ferrum/accessibility/ax_node.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Ferrum
class Accessibility
class AXNode
@params: Hash[String, untyped]

def initialize: (Hash[String, untyped] params) -> void

def role: () -> String?

def name: () -> String?

def description: () -> String?

def value: () -> untyped

def properties: () -> Hash[String, untyped]

def ignored?: () -> bool

def ignored_reasons: () -> Array[untyped]?

def node_id: () -> String?

def backend_dom_node_id: () -> Integer?

def child_ids: () -> Array[untyped]?

def to_h: () -> Hash[String, untyped]
end
end
end
2 changes: 2 additions & 0 deletions sig/ferrum/node.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ module Ferrum

def computed_style: () -> untyped

def axnode: () -> Accessibility::AXNode?

private

def bounding_rect_coordinates: () -> untyped
Expand Down
1 change: 1 addition & 0 deletions sig/ferrum/page.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Ferrum
attr_reader mouse: Mouse
attr_reader keyboard: Keyboard
attr_reader network: Network
attr_reader accessibility: Accessibility
attr_reader headers: Headers
attr_reader cookies: Cookies
attr_reader downloads: Downloads
Expand Down
Loading
Loading