summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts')
-rw-r--r--remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts146
1 files changed, 146 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts
new file mode 100644
index 0000000000..ffe8980d5e
--- /dev/null
+++ b/remote/test/puppeteer/packages/puppeteer-core/src/injected/TextContent.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright 2022 Google Inc.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+interface NonTrivialValueNode extends Node {
+ value: string;
+}
+
+const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
+
+/**
+ * Determines if the node has a non-trivial value property.
+ *
+ * @internal
+ */
+const isNonTrivialValueNode = (node: Node): node is NonTrivialValueNode => {
+ if (node instanceof HTMLSelectElement) {
+ return true;
+ }
+ if (node instanceof HTMLTextAreaElement) {
+ return true;
+ }
+ if (
+ node instanceof HTMLInputElement &&
+ !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
+ ) {
+ return true;
+ }
+ return false;
+};
+
+const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
+
+/**
+ * Determines whether a given node is suitable for text matching.
+ *
+ * @internal
+ */
+export const isSuitableNodeForTextMatching = (node: Node): boolean => {
+ return (
+ !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
+ );
+};
+
+/**
+ * @internal
+ */
+export interface TextContent {
+ // Contains the full text of the node.
+ full: string;
+ // Contains the text immediately beneath the node.
+ immediate: string[];
+}
+
+/**
+ * Maps {@link Node}s to their computed {@link TextContent}.
+ */
+const textContentCache = new WeakMap<Node, TextContent>();
+const eraseFromCache = (node: Node | null) => {
+ while (node) {
+ textContentCache.delete(node);
+ if (node instanceof ShadowRoot) {
+ node = node.host;
+ } else {
+ node = node.parentNode;
+ }
+ }
+};
+
+/**
+ * Erases the cache when the tree has mutated text.
+ */
+const observedNodes = new WeakSet<Node>();
+const textChangeObserver = new MutationObserver(mutations => {
+ for (const mutation of mutations) {
+ eraseFromCache(mutation.target);
+ }
+});
+
+/**
+ * Builds the text content of a node using some custom logic.
+ *
+ * @remarks
+ * The primary reason this function exists is due to {@link ShadowRoot}s not having
+ * text content.
+ *
+ * @internal
+ */
+export const createTextContent = (root: Node): TextContent => {
+ let value = textContentCache.get(root);
+ if (value) {
+ return value;
+ }
+ value = {full: '', immediate: []};
+ if (!isSuitableNodeForTextMatching(root)) {
+ return value;
+ }
+
+ let currentImmediate = '';
+ if (isNonTrivialValueNode(root)) {
+ value.full = root.value;
+ value.immediate.push(root.value);
+
+ root.addEventListener(
+ 'input',
+ event => {
+ eraseFromCache(event.target as HTMLInputElement);
+ },
+ {once: true, capture: true}
+ );
+ } else {
+ for (let child = root.firstChild; child; child = child.nextSibling) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ value.full += child.nodeValue ?? '';
+ currentImmediate += child.nodeValue ?? '';
+ continue;
+ }
+ if (currentImmediate) {
+ value.immediate.push(currentImmediate);
+ }
+ currentImmediate = '';
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ value.full += createTextContent(child).full;
+ }
+ }
+ if (currentImmediate) {
+ value.immediate.push(currentImmediate);
+ }
+ if (root instanceof Element && root.shadowRoot) {
+ value.full += createTextContent(root.shadowRoot).full;
+ }
+
+ if (!observedNodes.has(root)) {
+ textChangeObserver.observe(root, {
+ childList: true,
+ characterData: true,
+ subtree: true,
+ });
+ observedNodes.add(root);
+ }
+ }
+ textContentCache.set(root, value);
+ return value;
+};