|
1 | 1 | let refMap = new Map(); |
| 2 | +let metadataMap = new Map(); |
2 | 3 | let screenRect = null; |
3 | 4 | let lastUpdated = 0; |
4 | 5 | export function updateRefMap(nodes) { |
@@ -35,9 +36,107 @@ export function getRefMapAge() { |
35 | 36 | } |
36 | 37 | export function clearRefMap() { |
37 | 38 | refMap.clear(); |
| 39 | + metadataMap.clear(); |
38 | 40 | screenRect = null; |
39 | 41 | lastUpdated = 0; |
40 | 42 | } |
41 | 43 | export function hasRefMap() { |
42 | 44 | return refMap.size > 0; |
43 | 45 | } |
| 46 | +export function flattenXCUITree(tree) { |
| 47 | + const nodes = []; |
| 48 | + const localRefMap = new Map(); |
| 49 | + let counter = 0; |
| 50 | + const walk = (node) => { |
| 51 | + if (!node || typeof node !== 'object') |
| 52 | + return; |
| 53 | + if (node.frame) { |
| 54 | + const id = `e${counter}`; |
| 55 | + counter++; |
| 56 | + const rect = { |
| 57 | + x: node.frame.x, |
| 58 | + y: node.frame.y, |
| 59 | + width: node.frame.width, |
| 60 | + height: node.frame.height, |
| 61 | + }; |
| 62 | + const flat = { |
| 63 | + ref: `@${id}`, |
| 64 | + type: node.type ?? '', |
| 65 | + rect, |
| 66 | + }; |
| 67 | + if (node.label !== undefined) |
| 68 | + flat.label = node.label; |
| 69 | + if (node.identifier !== undefined) |
| 70 | + flat.identifier = node.identifier; |
| 71 | + if (node.enabled !== undefined) |
| 72 | + flat.enabled = node.enabled; |
| 73 | + if (node.hittable !== undefined) |
| 74 | + flat.hittable = node.hittable; |
| 75 | + nodes.push(flat); |
| 76 | + localRefMap.set(id, rect); |
| 77 | + } |
| 78 | + if (Array.isArray(node.children)) { |
| 79 | + for (const child of node.children) |
| 80 | + walk(child); |
| 81 | + } |
| 82 | + }; |
| 83 | + walk(tree); |
| 84 | + return { nodes, refMap: localRefMap }; |
| 85 | +} |
| 86 | +export function updateRefMapFromFlat(nodes) { |
| 87 | + refMap.clear(); |
| 88 | + metadataMap.clear(); |
| 89 | + screenRect = null; |
| 90 | + for (const node of nodes) { |
| 91 | + if (!node.ref || !node.rect) |
| 92 | + continue; |
| 93 | + const key = node.ref.startsWith('@') ? node.ref.slice(1) : node.ref; |
| 94 | + refMap.set(key, node.rect); |
| 95 | + const meta = { type: node.type }; |
| 96 | + if (node.label !== undefined) |
| 97 | + meta.label = node.label; |
| 98 | + if (node.identifier !== undefined) |
| 99 | + meta.identifier = node.identifier; |
| 100 | + metadataMap.set(key, meta); |
| 101 | + if (!screenRect && node.rect.x === 0 && node.rect.y === 0 && node.rect.width > 300) { |
| 102 | + screenRect = node.rect; |
| 103 | + } |
| 104 | + } |
| 105 | + lastUpdated = Date.now(); |
| 106 | +} |
| 107 | +export function getCachedMetadata(ref) { |
| 108 | + const key = ref.startsWith('@') ? ref.slice(1) : ref; |
| 109 | + return metadataMap.get(key) ?? null; |
| 110 | +} |
| 111 | +function metadataMatches(a, b) { |
| 112 | + return a.type === b.type && a.label === b.label && a.identifier === b.identifier; |
| 113 | +} |
| 114 | +function flatNodeMetadata(node) { |
| 115 | + const meta = { type: node.type }; |
| 116 | + if (node.label !== undefined) |
| 117 | + meta.label = node.label; |
| 118 | + if (node.identifier !== undefined) |
| 119 | + meta.identifier = node.identifier; |
| 120 | + return meta; |
| 121 | +} |
| 122 | +export function isRefStale(ref, newNodes) { |
| 123 | + const cached = getCachedMetadata(ref); |
| 124 | + if (!cached) |
| 125 | + return true; |
| 126 | + const target = ref.startsWith('@') ? ref : `@${ref}`; |
| 127 | + const fresh = newNodes.find((n) => n.ref === target); |
| 128 | + if (!fresh) |
| 129 | + return true; |
| 130 | + return !metadataMatches(cached, flatNodeMetadata(fresh)); |
| 131 | +} |
| 132 | +export function findNewRefByMetadata(oldRef, newNodes) { |
| 133 | + const cached = getCachedMetadata(oldRef); |
| 134 | + if (!cached) |
| 135 | + return null; |
| 136 | + for (const node of newNodes) { |
| 137 | + if (metadataMatches(cached, flatNodeMetadata(node))) { |
| 138 | + return node.ref; |
| 139 | + } |
| 140 | + } |
| 141 | + return null; |
| 142 | +} |
0 commit comments