tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ElementHandle.ts (5967B)


      1 /**
      2 * @license
      3 * Copyright 2019 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {Protocol} from 'devtools-protocol';
      8 
      9 import type {CDPSession} from '../api/CDPSession.js';
     10 import {
     11  bindIsolatedHandle,
     12  ElementHandle,
     13  type AutofillData,
     14 } from '../api/ElementHandle.js';
     15 import type {AwaitableIterable} from '../common/types.js';
     16 import {debugError} from '../common/util.js';
     17 import {environment} from '../environment.js';
     18 import {assert} from '../util/assert.js';
     19 import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
     20 import {throwIfDisposed} from '../util/decorators.js';
     21 
     22 import type {CdpFrame} from './Frame.js';
     23 import type {FrameManager} from './FrameManager.js';
     24 import type {IsolatedWorld} from './IsolatedWorld.js';
     25 import {CdpJSHandle} from './JSHandle.js';
     26 
     27 const NON_ELEMENT_NODE_ROLES = new Set(['StaticText', 'InlineTextBox']);
     28 
     29 /**
     30 * The CdpElementHandle extends ElementHandle now to keep compatibility
     31 * with `instanceof` because of that we need to have methods for
     32 * CdpJSHandle to in this implementation as well.
     33 *
     34 * @internal
     35 */
     36 export class CdpElementHandle<
     37  ElementType extends Node = Element,
     38 > extends ElementHandle<ElementType> {
     39  declare protected readonly handle: CdpJSHandle<ElementType>;
     40  #backendNodeId?: number;
     41 
     42  constructor(
     43    world: IsolatedWorld,
     44    remoteObject: Protocol.Runtime.RemoteObject,
     45  ) {
     46    super(new CdpJSHandle(world, remoteObject));
     47  }
     48 
     49  override get realm(): IsolatedWorld {
     50    return this.handle.realm;
     51  }
     52 
     53  get client(): CDPSession {
     54    return this.handle.client;
     55  }
     56 
     57  override remoteObject(): Protocol.Runtime.RemoteObject {
     58    return this.handle.remoteObject();
     59  }
     60 
     61  get #frameManager(): FrameManager {
     62    return this.frame._frameManager;
     63  }
     64 
     65  override get frame(): CdpFrame {
     66    return this.realm.environment as CdpFrame;
     67  }
     68 
     69  override async contentFrame(
     70    this: ElementHandle<HTMLIFrameElement>,
     71  ): Promise<CdpFrame>;
     72 
     73  @throwIfDisposed()
     74  override async contentFrame(): Promise<CdpFrame | null> {
     75    const nodeInfo = await this.client.send('DOM.describeNode', {
     76      objectId: this.id,
     77    });
     78    if (typeof nodeInfo.node.frameId !== 'string') {
     79      return null;
     80    }
     81    return this.#frameManager.frame(nodeInfo.node.frameId);
     82  }
     83 
     84  @throwIfDisposed()
     85  @bindIsolatedHandle
     86  override async scrollIntoView(
     87    this: CdpElementHandle<Element>,
     88  ): Promise<void> {
     89    await this.assertConnectedElement();
     90    try {
     91      await this.client.send('DOM.scrollIntoViewIfNeeded', {
     92        objectId: this.id,
     93      });
     94    } catch (error) {
     95      debugError(error);
     96      // Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
     97      await super.scrollIntoView();
     98    }
     99  }
    100 
    101  @throwIfDisposed()
    102  @bindIsolatedHandle
    103  override async uploadFile(
    104    this: CdpElementHandle<HTMLInputElement>,
    105    ...files: string[]
    106  ): Promise<void> {
    107    const isMultiple = await this.evaluate(element => {
    108      return element.multiple;
    109    });
    110    assert(
    111      files.length <= 1 || isMultiple,
    112      'Multiple file uploads only work with <input type=file multiple>',
    113    );
    114 
    115    // Locate all files and confirm that they exist.
    116    const path = environment.value.path;
    117    if (path) {
    118      files = files.map(filePath => {
    119        if (
    120          path.win32.isAbsolute(filePath) ||
    121          path.posix.isAbsolute(filePath)
    122        ) {
    123          return filePath;
    124        } else {
    125          return path.resolve(filePath);
    126        }
    127      });
    128    }
    129 
    130    /**
    131     * The zero-length array is a special case, it seems that
    132     * DOM.setFileInputFiles does not actually update the files in that case, so
    133     * the solution is to eval the element value to a new FileList directly.
    134     */
    135    if (files.length === 0) {
    136      // XXX: These events should converted to trusted events. Perhaps do this
    137      // in `DOM.setFileInputFiles`?
    138      await this.evaluate(element => {
    139        element.files = new DataTransfer().files;
    140 
    141        // Dispatch events for this case because it should behave akin to a user action.
    142        element.dispatchEvent(
    143          new Event('input', {bubbles: true, composed: true}),
    144        );
    145        element.dispatchEvent(new Event('change', {bubbles: true}));
    146      });
    147      return;
    148    }
    149 
    150    const {
    151      node: {backendNodeId},
    152    } = await this.client.send('DOM.describeNode', {
    153      objectId: this.id,
    154    });
    155    await this.client.send('DOM.setFileInputFiles', {
    156      objectId: this.id,
    157      files,
    158      backendNodeId,
    159    });
    160  }
    161 
    162  @throwIfDisposed()
    163  override async autofill(data: AutofillData): Promise<void> {
    164    const nodeInfo = await this.client.send('DOM.describeNode', {
    165      objectId: this.handle.id,
    166    });
    167    const fieldId = nodeInfo.node.backendNodeId;
    168    const frameId = this.frame._id;
    169    await this.client.send('Autofill.trigger', {
    170      fieldId,
    171      frameId,
    172      card: data.creditCard,
    173    });
    174  }
    175 
    176  override async *queryAXTree(
    177    name?: string | undefined,
    178    role?: string | undefined,
    179  ): AwaitableIterable<ElementHandle<Node>> {
    180    const {nodes} = await this.client.send('Accessibility.queryAXTree', {
    181      objectId: this.id,
    182      accessibleName: name,
    183      role,
    184    });
    185 
    186    const results = nodes.filter(node => {
    187      if (node.ignored) {
    188        return false;
    189      }
    190      if (!node.role) {
    191        return false;
    192      }
    193      if (NON_ELEMENT_NODE_ROLES.has(node.role.value)) {
    194        return false;
    195      }
    196      return true;
    197    });
    198 
    199    return yield* AsyncIterableUtil.map(results, node => {
    200      return this.realm.adoptBackendNode(node.backendDOMNodeId) as Promise<
    201        ElementHandle<Node>
    202      >;
    203    });
    204  }
    205 
    206  override async backendNodeId(): Promise<number> {
    207    if (this.#backendNodeId) {
    208      return this.#backendNodeId;
    209    }
    210    const {node} = await this.client.send('DOM.describeNode', {
    211      objectId: this.handle.id,
    212    });
    213    this.#backendNodeId = node.backendNodeId;
    214    return this.#backendNodeId;
    215  }
    216 }