// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
struct ParityCase {
name : String
host_focused : Bool
updates : Array[@common.TreeUpdate]
} derive(FromJson)
///|
struct FocusMove {
old_focus : String?
new_focus : String?
} derive(ToJson)
///|
struct ChangesObservation {
added : Array[String]
updated : Array[String]
removed : Array[String]
focus_moves : Array[FocusMove]
} derive(ToJson)
///|
struct RecordingHandler {
added : Array[String]
updated : Array[String]
removed : Array[String]
focus_moves : Array[FocusMove]
}
///|
fn RecordingHandler::new() -> RecordingHandler {
{ added: [], updated: [], removed: [], focus_moves: [], }
}
///|
fn local_id(node : @consumer.NodeRef) -> String {
node.id().to_components().0.to_u64().to_string()
}
///|
fn optional_local_id(node : @consumer.NodeRef?) -> String? {
node.map(local_id)
}
///|
impl @consumer.ChangeHandler for RecordingHandler with fn node_added(self, node) {
self.added.push(local_id(node))
}
///|
impl @consumer.ChangeHandler for RecordingHandler with fn node_updated(
self,
_old_node,
new_node,
) {
self.updated.push(local_id(new_node))
}
///|
impl @consumer.ChangeHandler for RecordingHandler with fn focus_moved(
self,
old_focus,
new_focus,
) {
self.focus_moves.push({
old_focus: optional_local_id(old_focus),
new_focus: optional_local_id(new_focus),
})
}
///|
impl @consumer.ChangeHandler for RecordingHandler with fn node_removed(
self,
node,
) {
self.removed.push(local_id(node))
}
///|
fn RecordingHandler::observation(self : RecordingHandler) -> ChangesObservation {
self.added.sort()
self.updated.sort()
self.removed.sort()
{
added: self.added,
updated: self.updated,
removed: self.removed,
focus_moves: self.focus_moves,
}
}
///|
fn empty_changes() -> ChangesObservation {
{ added: [], updated: [], removed: [], focus_moves: [], }
}
///|
struct NodeObservation {
id : String
parent_id : String?
role : String
label : String?
value : String?
is_hidden : Bool
is_focused : Bool
is_focused_in_tree : Bool
common_filter : String
children : Array[String]
children_reversed : Array[String]
children_mixed : Array[String]
filtered_children : Array[String]
filtered_children_reversed : Array[String]
filtered_children_mixed : Array[String]
following_siblings : Array[String]
following_filtered_siblings : Array[String]
preceding_siblings : Array[String]
preceding_filtered_siblings : Array[String]
labelled_by : Array[String]
controls : Array[String]
items : Array[String]
} derive(ToJson)
///|
struct TreeObservation {
root : String
focus : String?
focus_in_tree : String
active_dialog : String?
nodes : Array[NodeObservation]
} derive(ToJson)
///|
struct StepObservation {
state : TreeObservation
changes : ChangesObservation
} derive(ToJson)
///|
struct CaseObservation {
name : String
steps : Array[StepObservation]
} derive(ToJson)
///|
fn ids(iter : @consumer.DoubleEndedIter[@consumer.NodeRef]) -> Array[String] {
iter.iter().map(local_id).to_array()
}
///|
fn reversed_ids(
iter : @consumer.DoubleEndedIter[@consumer.NodeRef],
) -> Array[String] {
iter.rev_iter().map(local_id).to_array()
}
///|
fn mixed_ids(
iter : @consumer.DoubleEndedIter[@consumer.NodeRef],
) -> Array[String] {
let result : Array[String] = []
let mut take_front = true
for ;; {
let next = if take_front { iter.next() } else { iter.next_back() }
match next {
None => break
Some(node) => {
result.push(local_id(node))
take_front = !take_front
}
}
}
result
}
///|
fn filter_name(node : @consumer.NodeRef) -> String {
match @consumer.common_filter(node) {
Include => "Include"
ExcludeNode => "ExcludeNode"
ExcludeSubtree => "ExcludeSubtree"
}
}
///|
fn observe_node(node : @consumer.NodeRef) -> NodeObservation {
{
id: local_id(node),
parent_id: optional_local_id(node.parent()),
role: node.role().to_string(),
label: node.label(),
value: node.value(),
is_hidden: node.is_hidden(),
is_focused: node.is_focused(),
is_focused_in_tree: node.is_focused_in_tree(),
common_filter: filter_name(node),
children: ids(node.children()),
children_reversed: reversed_ids(node.children()),
children_mixed: mixed_ids(node.children()),
filtered_children: ids(node.filtered_children(@consumer.common_filter)),
filtered_children_reversed: reversed_ids(
node.filtered_children(@consumer.common_filter),
),
filtered_children_mixed: mixed_ids(
node.filtered_children(@consumer.common_filter),
),
following_siblings: ids(node.following_siblings()),
following_filtered_siblings: ids(
node.following_filtered_siblings(@consumer.common_filter),
),
preceding_siblings: ids(node.preceding_siblings()),
preceding_filtered_siblings: ids(
node.preceding_filtered_siblings(@consumer.common_filter),
),
labelled_by: ids(node.labelled_by()),
controls: ids(node.controls()),
items: ids(node.items(@consumer.common_filter)),
}
}
///|
fn observe_state(state : @consumer.TreeState) -> TreeObservation {
let stack : Array[@consumer.NodeRef] = [state.root()]
let nodes : Array[NodeObservation] = []
while stack.pop() is Some(node) {
let children = node.children()
while children.next_back() is Some(child) {
stack.push(child)
}
nodes.push(observe_node(node))
}
{
root: local_id(state.root()),
focus: optional_local_id(state.focus()),
focus_in_tree: local_id(state.focus_in_tree()),
active_dialog: optional_local_id(state.active_dialog()),
nodes,
}
}
///|
fn run_case(case : ParityCase) -> CaseObservation {
if case.updates.length() == 0 {
abort("parity case \"" + case.name + "\" has no updates")
}
let tree = @consumer.Tree(case.updates[0], case.host_focused)
let steps : Array[StepObservation] = [
{ state: observe_state(tree.state()), changes: empty_changes(), },
]
for update_index in 1..")
}
let source = @fs.read_file_to_string(args[1])
let cases : Array[ParityCase] = @json.from_json(@json.parse(source))
let observations = cases.map(run_case)
println(@json.to_json(observations).stringify(indent=2))
}