repo_description: Chrome DevTools for coding agents
repo_topics: browser chrome chrome-devtools debugging devtools mcp mcp-server puppeteer
code: /**
   * @license
   * Copyright 2025 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  import type {TextSnapshot} from '../TextSnapshot.js';
  import type {TextSnapshotNode} from '../types.js';
  
  export class SnapshotFormatter {
    #snapshot: TextSnapshot;
  
    constructor(snapshot: TextSnapshot) {
      this.#snapshot = snapshot;
    }
  
    toString(): string {
      const chunks: string[] = [];
      const root = this.#snapshot.root;
  
      // Top-level content of the snapshot.
      if (
        this.#snapshot.verbose &&
        this.#snapshot.hasSelectedElement &&
        !this.#snapshot.selectedElementUid
      ) {
        chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
  Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`);
      }
  
      chunks.push(this.#formatNode(root, 0));
      return chunks.join('');
    }
  
    toJSON(): object {
      return this.#nodeToJSON(this.#snapshot.root);
    }
  
    #formatNode(node: TextSnapshotNode, depth = 0): string {
      const chunks: string[] = [];
      const attributes = this.#getAttributes(node);
      const line =
        ' '.repeat(depth * 2) +
        attributes.join(' ') +
        (node.id === this.#snapshot.selectedElementUid
          ? ' [selected in the DevTools Elements panel]'
          : '') +
        '\n';
      chunks.push(line);
  
      for (const child of node.children) {
        chunks.push(this.#formatNode(child, depth + 1));
      }
      return chunks.join('');
    }
  
    #nodeToJSON(node: TextSnapshotNode): object {
      const rawAttrs = this.#getAttributesMap(node);
      const children = node.children.map(child => this.#nodeToJSON(child));
      const result: Record<string, unknown> = structuredClone(rawAttrs);
      if (children.length > 0) {
        result.children = children;
      }
      return result;
    }
  
    #getAttributes(serializedAXNodeRoot: TextSnapshotNode): string[] {
      const attributes = [`uid=${serializedAXNodeRoot.id}`];
  
      if (serializedAXNodeRoot.role) {
        attributes.push(
          serializedAXNodeRoot.role === 'none'
            ? 'ignored'
            : serializedAXNodeRoot.role,
        );
      }
      if (serializedAXNodeRoot.name) {
        attributes.push(`"${serializedAXNodeRoot.name}"`);
      }
  
      const simpleAttrs = this.#getAttributesMap(
        serializedAXNodeRoot,
        /* excludeSpecial */ true,
      );
  
      for (const attr of Object.keys(serializedAXNodeRoot).sort()) {
        if (excludedAttributes.has(attr)) {
          continue;
        }
  
        const mapped = booleanPropertyMap[attr];
        if (mapped && simpleAttrs[mapped]) {
          attributes.push(mapped);
        }
  
        const val = simpleAttrs[attr];
        if (val === true) {
          attributes.push(attr);
        } else if (typeof val === 'string' || typeof val === 'number') {
          attributes.push(`${attr}="${val}"`);
        }
      }
  
      return attributes;
    }
  
    #getAttributesMap(
      node: TextSnapshotNode,
      excludeSpecial = false,
    ): Record<string, unknown> {
      const result: Record<string, unknown> = {};
      if (!excludeSpecial) {
        result.id = node.id;
        if (node.role) {
          result.role = node.role;
        }
        if (node.name) {
          result.name = node.name;
        }
      }
  
      // Re-implementing the exact logic from original function for #getAttributes to be safe:
      return {
        ...result,
        ...this.#extractedAttributes(node),
      };
    }
  
    #extractedAttributes(node: TextSnapshotNode): Record<string, unknown> {
      const result: Record<string, unknown> = {};
  
      for (const attr of Object.keys(node).sort()) {
        if (excludedAttributes.has(attr)) {
          continue;
        }
        const value = (node as unknown as Record<string, unknown>)[attr];
        if (typeof value === 'boolean') {
          if (booleanPropertyMap[attr]) {
            result[booleanPropertyMap[attr]] = true;
          }
          if (value) {
            result[attr] = true;
          }
        } else if (typeof value === 'string' || typeof value === 'number') {
          result[attr] = value;
        }
      }
      return result;
    }
  }
  
  const booleanPropertyMap: Record<string, string> = {
    disabled: 'disableable',
    expanded: 'expandable',
    focused: 'focusable',
    selected: 'selectable',
  };
  
  const excludedAttributes = new Set([
    'id',
    'role',
    'name',
    'elementHandle',
    'children',
    'backendNodeId',
    'loaderId',
  ]);
  