| 71 | const seenBackendNodeIds = new Set<number>(); |
| 72 | |
| 73 | const assignIds = (node: SerializedAXNode): TextSnapshotNode => { |
| 74 | let id = ''; |
| 75 | // @ts-expect-error untyped backendNodeId. |
| 76 | const backendNodeId: number = node.backendNodeId; |
| 77 | // @ts-expect-error untyped loaderId. |
| 78 | const uniqueBackendId = `${node.loaderId}_${backendNodeId}`; |
| 79 | const existingMcpId = uniqueBackendNodeIdToMcpId.get(uniqueBackendId); |
| 80 | if (existingMcpId !== undefined) { |
| 81 | // Re-use MCP exposed ID if the uniqueId is the same. |
| 82 | id = existingMcpId; |
| 83 | } else { |
| 84 | // Only generate a new ID if we have not seen the node before. |
| 85 | id = `${snapshotId}_${idCounter++}`; |
| 86 | uniqueBackendNodeIdToMcpId.set(uniqueBackendId, id); |
| 87 | } |
| 88 | seenUniqueIds.add(uniqueBackendId); |
| 89 | seenBackendNodeIds.add(backendNodeId); |
| 90 | |
| 91 | const nodeWithId: TextSnapshotNode = { |
| 92 | ...node, |
| 93 | id, |
| 94 | children: node.children |
| 95 | ? node.children.map(child => assignIds(child)) |
| 96 | : [], |
| 97 | }; |
| 98 | |
| 99 | // The AXNode for an option doesn't contain its `value`. |
| 100 | // Therefore, set text content of the option as value. |
| 101 | if (node.role === 'option') { |
| 102 | const optionText = node.name; |
| 103 | if (optionText) { |
| 104 | nodeWithId.value = optionText.toString(); |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | idToNode.set(nodeWithId.id, nodeWithId); |
| 109 | return nodeWithId; |
| 110 | }; |
| 111 | |
| 112 | const rootNodeWithId = assignIds(rootNode); |
| 113 | |