(
args: string[],
session: TabSession,
securityOpts?: { splitForScoped?: boolean },
)
| 136 | * Take an accessibility snapshot and build the ref map. |
| 137 | */ |
| 138 | export async function handleSnapshot( |
| 139 | args: string[], |
| 140 | session: TabSession, |
| 141 | securityOpts?: { splitForScoped?: boolean }, |
| 142 | ): Promise<string> { |
| 143 | const opts = parseSnapshotArgs(args); |
| 144 | const page = session.getPage(); |
| 145 | // Frame-aware target for accessibility tree |
| 146 | const target = session.getActiveFrameOrPage(); |
| 147 | const inFrame = session.getFrame() !== null; |
| 148 | |
| 149 | // Get accessibility tree via ariaSnapshot |
| 150 | let rootLocator: Locator; |
| 151 | if (opts.selector) { |
| 152 | rootLocator = target.locator(opts.selector); |
| 153 | const count = await rootLocator.count(); |
| 154 | if (count === 0) throw new Error(`Selector not found: ${opts.selector}`); |
| 155 | } else { |
| 156 | rootLocator = target.locator('body'); |
| 157 | } |
| 158 | |
| 159 | const ariaText = await rootLocator.ariaSnapshot(); |
| 160 | if (!ariaText || ariaText.trim().length === 0) { |
| 161 | session.setRefMap(new Map()); |
| 162 | return '(no accessible elements found)'; |
| 163 | } |
| 164 | |
| 165 | // Parse the ariaSnapshot output |
| 166 | const lines = ariaText.split('\n'); |
| 167 | const refMap = new Map<string, RefEntry>(); |
| 168 | const output: string[] = []; |
| 169 | let refCounter = 1; |
| 170 | |
| 171 | // Track role+name occurrences for nth() disambiguation |
| 172 | const roleNameCounts = new Map<string, number>(); |
| 173 | const roleNameSeen = new Map<string, number>(); |
| 174 | |
| 175 | // First pass: count role+name pairs for disambiguation |
| 176 | for (const line of lines) { |
| 177 | const node = parseLine(line); |
| 178 | if (!node) continue; |
| 179 | const key = `${node.role}:${node.name || ''}`; |
| 180 | roleNameCounts.set(key, (roleNameCounts.get(key) || 0) + 1); |
| 181 | } |
| 182 | |
| 183 | // Second pass: assign refs and build locators |
| 184 | for (const line of lines) { |
| 185 | const node = parseLine(line); |
| 186 | if (!node) continue; |
| 187 | |
| 188 | const depth = Math.floor(node.indent / 2); |
| 189 | const isInteractive = INTERACTIVE_ROLES.has(node.role); |
| 190 | |
| 191 | // Depth filter |
| 192 | if (opts.depth !== undefined && depth > opts.depth) continue; |
| 193 | |
| 194 | // Interactive filter: skip non-interactive but still count for locator indices |
| 195 | if (opts.interactive && !isInteractive) { |
no test coverage detected