chore: refactor code around text selectors (#19278)
This commit is contained in:
parent
84a0aaaaff
commit
48182a4eb2
|
|
@ -22,7 +22,7 @@ import { createRoleEngine } from './roleSelectorEngine';
|
||||||
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
import { parseAttributeSelector } from '../isomorphic/selectorParser';
|
||||||
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from '../isomorphic/selectorParser';
|
||||||
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
import { allEngineNames, parseSelector, stringifySelector } from '../isomorphic/selectorParser';
|
||||||
import { type TextMatcher, elementMatchesText, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher, elementText, createStrictFullTextMatcher } from './selectorUtils';
|
import { type TextMatcher, elementMatchesText, elementText, type ElementText } from './selectorUtils';
|
||||||
import { SelectorEvaluatorImpl } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl } from './selectorEvaluator';
|
||||||
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
import { enclosingShadowRootOrDocument, isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||||
import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
import type { CSSComplexSelectorList } from '../isomorphic/cssParser';
|
||||||
|
|
@ -33,6 +33,7 @@ import { getAriaCheckedStrict, getAriaDisabled, getAriaRole, getElementAccessibl
|
||||||
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||||
import { asLocator } from '../isomorphic/locatorGenerators';
|
import { asLocator } from '../isomorphic/locatorGenerators';
|
||||||
import type { Language } from '../isomorphic/locatorGenerators';
|
import type { Language } from '../isomorphic/locatorGenerators';
|
||||||
|
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
|
|
||||||
type Predicate<T> = (progress: InjectedScriptProgress) => T | symbol;
|
type Predicate<T> = (progress: InjectedScriptProgress) => T | symbol;
|
||||||
|
|
||||||
|
|
@ -64,16 +65,12 @@ export type InjectedScriptPoll<T> = {
|
||||||
export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked';
|
export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked';
|
||||||
export type ElementState = ElementStateWithoutStable | 'stable';
|
export type ElementState = ElementStateWithoutStable | 'stable';
|
||||||
|
|
||||||
export interface SelectorEngineV2 {
|
|
||||||
queryAll(root: SelectorRoot, body: any): Element[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HitTargetInterceptionResult = {
|
export type HitTargetInterceptionResult = {
|
||||||
stop: () => 'done' | { hitTargetDescription: string };
|
stop: () => 'done' | { hitTargetDescription: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InjectedScript {
|
export class InjectedScript {
|
||||||
private _engines: Map<string, SelectorEngineV2>;
|
private _engines: Map<string, SelectorEngine>;
|
||||||
_evaluator: SelectorEvaluatorImpl;
|
_evaluator: SelectorEvaluatorImpl;
|
||||||
private _stableRafCount: number;
|
private _stableRafCount: number;
|
||||||
private _browserName: string;
|
private _browserName: string;
|
||||||
|
|
@ -247,17 +244,16 @@ export class InjectedScript {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createCSSEngine(): SelectorEngineV2 {
|
private _createCSSEngine(): SelectorEngine {
|
||||||
const evaluator = this._evaluator;
|
|
||||||
return {
|
return {
|
||||||
queryAll(root: SelectorRoot, body: any) {
|
queryAll: (root: SelectorRoot, body: any) => {
|
||||||
return evaluator.query({ scope: root as Document | Element, pierceShadow: true }, body);
|
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, body);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createTextEngine(shadow: boolean, internal: boolean): SelectorEngine {
|
private _createTextEngine(shadow: boolean, internal: boolean): SelectorEngine {
|
||||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
const queryAll = (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const { matcher, kind } = createTextMatcher(selector, internal);
|
const { matcher, kind } = createTextMatcher(selector, internal);
|
||||||
const result: Element[] = [];
|
const result: Element[] = [];
|
||||||
let lastDidNotMatchSelf: Element | null = null;
|
let lastDidNotMatchSelf: Element | null = null;
|
||||||
|
|
@ -280,22 +276,16 @@ export class InjectedScript {
|
||||||
appendElement(element);
|
appendElement(element);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
return { queryAll };
|
||||||
return {
|
|
||||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
|
||||||
return queryList(root, selector);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createInternalHasTextEngine(): SelectorEngine {
|
private _createInternalHasTextEngine(): SelectorEngine {
|
||||||
const evaluator = this._evaluator;
|
|
||||||
return {
|
return {
|
||||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||||
return [];
|
return [];
|
||||||
const element = root as Element;
|
const element = root as Element;
|
||||||
const text = elementText(evaluator._cacheText, element);
|
const text = elementText(this._evaluator._cacheText, element);
|
||||||
const { matcher } = createTextMatcher(selector, true);
|
const { matcher } = createTextMatcher(selector, true);
|
||||||
return matcher(text) ? [element] : [];
|
return matcher(text) ? [element] : [];
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +293,6 @@ export class InjectedScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createInternalLabelEngine(): SelectorEngine {
|
private _createInternalLabelEngine(): SelectorEngine {
|
||||||
const evaluator = this._evaluator;
|
|
||||||
return {
|
return {
|
||||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const { matcher } = createTextMatcher(selector, true);
|
const { matcher } = createTextMatcher(selector, true);
|
||||||
|
|
@ -311,7 +300,7 @@ export class InjectedScript {
|
||||||
const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[];
|
const labels = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, 'label') as HTMLLabelElement[];
|
||||||
for (const label of labels) {
|
for (const label of labels) {
|
||||||
const control = label.control;
|
const control = label.control;
|
||||||
if (control && matcher(elementText(evaluator._cacheText, label)))
|
if (control && matcher(elementText(this._evaluator._cacheText, label)))
|
||||||
result.push(control);
|
result.push(control);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -320,7 +309,7 @@ export class InjectedScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createNamedAttributeEngine(): SelectorEngine {
|
private _createNamedAttributeEngine(): SelectorEngine {
|
||||||
const queryList = (root: SelectorRoot, selector: string): Element[] => {
|
const queryAll = (root: SelectorRoot, selector: string): Element[] => {
|
||||||
const parsed = parseAttributeSelector(selector, true);
|
const parsed = parseAttributeSelector(selector, true);
|
||||||
if (parsed.name || parsed.attributes.length !== 1)
|
if (parsed.name || parsed.attributes.length !== 1)
|
||||||
throw new Error('Malformed attribute selector: ' + selector);
|
throw new Error('Malformed attribute selector: ' + selector);
|
||||||
|
|
@ -336,15 +325,10 @@ export class InjectedScript {
|
||||||
const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, `[${name}]`);
|
const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, `[${name}]`);
|
||||||
return elements.filter(e => matcher(e.getAttribute(name)!));
|
return elements.filter(e => matcher(e.getAttribute(name)!));
|
||||||
};
|
};
|
||||||
|
return { queryAll };
|
||||||
return {
|
|
||||||
queryAll: (root: SelectorRoot, selector: string): Element[] => {
|
|
||||||
return queryList(root, selector);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createControlEngine(): SelectorEngineV2 {
|
private _createControlEngine(): SelectorEngine {
|
||||||
return {
|
return {
|
||||||
queryAll(root: SelectorRoot, body: any) {
|
queryAll(root: SelectorRoot, body: any) {
|
||||||
if (body === 'enter-frame')
|
if (body === 'enter-frame')
|
||||||
|
|
@ -363,7 +347,7 @@ export class InjectedScript {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createHasEngine(): SelectorEngineV2 {
|
private _createHasEngine(): SelectorEngine {
|
||||||
const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => {
|
const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => {
|
||||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -373,7 +357,7 @@ export class InjectedScript {
|
||||||
return { queryAll };
|
return { queryAll };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createVisibleEngine(): SelectorEngineV2 {
|
private _createVisibleEngine(): SelectorEngine {
|
||||||
const queryAll = (root: SelectorRoot, body: string) => {
|
const queryAll = (root: SelectorRoot, body: string) => {
|
||||||
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
if (root.nodeType !== 1 /* Node.ELEMENT_NODE */)
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -1369,8 +1353,8 @@ function cssUnquote(s: string): string {
|
||||||
function createTextMatcher(selector: string, internal: boolean): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } {
|
function createTextMatcher(selector: string, internal: boolean): { matcher: TextMatcher, kind: 'regex' | 'strict' | 'lax' } {
|
||||||
if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
|
if (selector[0] === '/' && selector.lastIndexOf('/') > 0) {
|
||||||
const lastSlash = selector.lastIndexOf('/');
|
const lastSlash = selector.lastIndexOf('/');
|
||||||
const matcher: TextMatcher = createRegexTextMatcher(selector.substring(1, lastSlash), selector.substring(lastSlash + 1));
|
const re = new RegExp(selector.substring(1, lastSlash), selector.substring(lastSlash + 1));
|
||||||
return { matcher, kind: 'regex' };
|
return { matcher: (elementText: ElementText) => re.test(elementText.full), kind: 'regex' };
|
||||||
}
|
}
|
||||||
const unquote = internal ? JSON.parse.bind(JSON) : cssUnquote;
|
const unquote = internal ? JSON.parse.bind(JSON) : cssUnquote;
|
||||||
let strict = false;
|
let strict = false;
|
||||||
|
|
@ -1387,9 +1371,20 @@ function createTextMatcher(selector: string, internal: boolean): { matcher: Text
|
||||||
selector = unquote(selector);
|
selector = unquote(selector);
|
||||||
strict = true;
|
strict = true;
|
||||||
}
|
}
|
||||||
if (strict)
|
selector = normalizeWhiteSpace(selector);
|
||||||
return { matcher: internal ? createStrictFullTextMatcher(selector) : createStrictTextMatcher(selector), kind: 'strict' };
|
if (strict) {
|
||||||
return { matcher: createLaxTextMatcher(selector), kind: 'lax' };
|
if (internal)
|
||||||
|
return { kind: 'strict', matcher: (elementText: ElementText) => normalizeWhiteSpace(elementText.full) === selector };
|
||||||
|
|
||||||
|
const strictTextNodeMatcher = (elementText: ElementText) => {
|
||||||
|
if (!selector && !elementText.immediate.length)
|
||||||
|
return true;
|
||||||
|
return elementText.immediate.some(s => normalizeWhiteSpace(s) === selector);
|
||||||
|
};
|
||||||
|
return { matcher: strictTextNodeMatcher, kind: 'strict' };
|
||||||
|
}
|
||||||
|
selector = selector.toLowerCase();
|
||||||
|
return { kind: 'lax', matcher: (elementText: ElementText) => normalizeWhiteSpace(elementText.full).toLowerCase().includes(selector) };
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExpectedTextMatcher {
|
class ExpectedTextMatcher {
|
||||||
|
|
@ -1430,7 +1425,7 @@ class ExpectedTextMatcher {
|
||||||
if (!s)
|
if (!s)
|
||||||
return s;
|
return s;
|
||||||
if (this._normalizeWhiteSpace)
|
if (this._normalizeWhiteSpace)
|
||||||
s = s.trim().replace(/\u200b/g, '').replace(/\s+/g, ' ');
|
s = normalizeWhiteSpace(s);
|
||||||
if (this._ignoreCase)
|
if (this._ignoreCase)
|
||||||
s = s.toLocaleLowerCase();
|
s = s.toLocaleLowerCase();
|
||||||
return s;
|
return s;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||||
import { matchesAttributePart } from './selectorUtils';
|
import { matchesAttributePart } from './selectorUtils';
|
||||||
import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
|
import { getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils';
|
||||||
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../isomorphic/selectorParser';
|
import { parseAttributeSelector, type AttributeSelectorPart, type AttributeSelectorOperator } from '../isomorphic/selectorParser';
|
||||||
|
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
|
|
||||||
const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'include-hidden'];
|
const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'include-hidden'];
|
||||||
kSupportedAttributes.sort();
|
kSupportedAttributes.sort();
|
||||||
|
|
@ -155,9 +156,9 @@ export function createRoleEngine(internal: boolean): SelectorEngine {
|
||||||
}
|
}
|
||||||
if (nameAttr !== undefined) {
|
if (nameAttr !== undefined) {
|
||||||
// Always normalize whitespace in the accessible name.
|
// Always normalize whitespace in the accessible name.
|
||||||
const accessibleName = getElementAccessibleName(element, includeHidden, hiddenCache).trim().replace(/\s+/g, ' ');
|
const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, includeHidden, hiddenCache));
|
||||||
if (typeof nameAttr.value === 'string')
|
if (typeof nameAttr.value === 'string')
|
||||||
nameAttr.value = nameAttr.value.trim().replace(/\s+/g, ' ');
|
nameAttr.value = normalizeWhiteSpace(nameAttr.value);
|
||||||
// internal:role assumes that [name="foo"i] also means substring.
|
// internal:role assumes that [name="foo"i] also means substring.
|
||||||
if (internal && !nameAttr.caseSensitive && nameAttr.op === '=')
|
if (internal && !nameAttr.caseSensitive && nameAttr.op === '=')
|
||||||
nameAttr.op = '*=';
|
nameAttr.op = '*=';
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@
|
||||||
export type SelectorRoot = Element | ShadowRoot | Document;
|
export type SelectorRoot = Element | ShadowRoot | Document;
|
||||||
|
|
||||||
export interface SelectorEngine {
|
export interface SelectorEngine {
|
||||||
queryAll(root: SelectorRoot, selector: string): Element[];
|
queryAll(root: SelectorRoot, selector: string | any): Element[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ import type { CSSComplexSelector, CSSSimpleSelector, CSSComplexSelectorList, CSS
|
||||||
import { customCSSNames } from '../isomorphic/selectorParser';
|
import { customCSSNames } from '../isomorphic/selectorParser';
|
||||||
import { isElementVisible, parentElementOrShadowHost } from './domUtils';
|
import { isElementVisible, parentElementOrShadowHost } from './domUtils';
|
||||||
import { type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
import { type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
|
||||||
import { createLaxTextMatcher, createRegexTextMatcher, createStrictTextMatcher, elementMatchesText, elementText, shouldSkipForTextMatching, type ElementText } from './selectorUtils';
|
import { elementMatchesText, elementText, shouldSkipForTextMatching, type ElementText } from './selectorUtils';
|
||||||
|
import { normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
|
|
||||||
type QueryContext = {
|
type QueryContext = {
|
||||||
scope: Element | Document;
|
scope: Element | Document;
|
||||||
|
|
@ -431,7 +432,8 @@ const textEngine: SelectorEngine = {
|
||||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||||
if (args.length !== 1 || typeof args[0] !== 'string')
|
if (args.length !== 1 || typeof args[0] !== 'string')
|
||||||
throw new Error(`"text" engine expects a single string`);
|
throw new Error(`"text" engine expects a single string`);
|
||||||
const matcher = createLaxTextMatcher(args[0]);
|
const text = normalizeWhiteSpace(args[0]).toLowerCase();
|
||||||
|
const matcher = (elementText: ElementText) => normalizeWhiteSpace(elementText.full).toLowerCase().includes(text);
|
||||||
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self';
|
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -440,7 +442,12 @@ const textIsEngine: SelectorEngine = {
|
||||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||||
if (args.length !== 1 || typeof args[0] !== 'string')
|
if (args.length !== 1 || typeof args[0] !== 'string')
|
||||||
throw new Error(`"text-is" engine expects a single string`);
|
throw new Error(`"text-is" engine expects a single string`);
|
||||||
const matcher = createStrictTextMatcher(args[0]);
|
const text = normalizeWhiteSpace(args[0]);
|
||||||
|
const matcher = (elementText: ElementText) => {
|
||||||
|
if (!text && !elementText.immediate.length)
|
||||||
|
return true;
|
||||||
|
return elementText.immediate.some(s => normalizeWhiteSpace(s) === text);
|
||||||
|
};
|
||||||
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) !== 'none';
|
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) !== 'none';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -449,7 +456,8 @@ const textMatchesEngine: SelectorEngine = {
|
||||||
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean {
|
||||||
if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
|
if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string'))
|
||||||
throw new Error(`"text-matches" engine expects a regexp body and optional regexp flags`);
|
throw new Error(`"text-matches" engine expects a regexp body and optional regexp flags`);
|
||||||
const matcher = createRegexTextMatcher(args[0], args.length === 2 ? args[1] : undefined);
|
const re = new RegExp(args[0], args.length === 2 ? args[1] : undefined);
|
||||||
|
const matcher = (elementText: ElementText) => re.test(elementText.full);
|
||||||
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self';
|
return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -460,7 +468,8 @@ const hasTextEngine: SelectorEngine = {
|
||||||
throw new Error(`"has-text" engine expects a single string`);
|
throw new Error(`"has-text" engine expects a single string`);
|
||||||
if (shouldSkipForTextMatching(element))
|
if (shouldSkipForTextMatching(element))
|
||||||
return false;
|
return false;
|
||||||
const matcher = createLaxTextMatcher(args[0]);
|
const text = normalizeWhiteSpace(args[0]).toLowerCase();
|
||||||
|
const matcher = (elementText: ElementText) => normalizeWhiteSpace(elementText.full).toLowerCase().includes(text);
|
||||||
return matcher(elementText((evaluator as SelectorEvaluatorImpl)._cacheText, element));
|
return matcher(elementText((evaluator as SelectorEvaluatorImpl)._cacheText, element));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { cssEscape, escapeForAttributeSelector, escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
|
import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, normalizeWhiteSpace } from '../../utils/isomorphic/stringUtils';
|
||||||
import { type InjectedScript } from './injectedScript';
|
import { type InjectedScript } from './injectedScript';
|
||||||
import { getAriaRole, getElementAccessibleName } from './roleUtils';
|
import { getAriaRole, getElementAccessibleName } from './roleUtils';
|
||||||
import { elementText } from './selectorUtils';
|
import { elementText } from './selectorUtils';
|
||||||
|
|
@ -216,7 +216,7 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, testI
|
||||||
function buildTextCandidates(injectedScript: InjectedScript, element: Element, isTargetNode: boolean, accessibleNameCache: Map<Element, boolean>): SelectorToken[][] {
|
function buildTextCandidates(injectedScript: InjectedScript, element: Element, isTargetNode: boolean, accessibleNameCache: Map<Element, boolean>): SelectorToken[][] {
|
||||||
if (element.nodeName === 'SELECT')
|
if (element.nodeName === 'SELECT')
|
||||||
return [];
|
return [];
|
||||||
const text = elementText(injectedScript._evaluator._cacheText, element).full.trim().replace(/\s+/g, ' ').substring(0, 80);
|
const text = normalizeWhiteSpace(elementText(injectedScript._evaluator._cacheText, element).full).substring(0, 80);
|
||||||
if (!text)
|
if (!text)
|
||||||
return [];
|
return [];
|
||||||
const candidates: SelectorToken[][] = [];
|
const candidates: SelectorToken[][] = [];
|
||||||
|
|
|
||||||
|
|
@ -50,38 +50,6 @@ export function matchesAttributePart(value: any, attr: AttributeSelectorPart) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function createLaxTextMatcher(text: string): TextMatcher {
|
|
||||||
text = text.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
||||||
return (elementText: ElementText) => {
|
|
||||||
const s = elementText.full.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
||||||
return s.includes(text);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createStrictTextMatcher(text: string): TextMatcher {
|
|
||||||
text = text.trim().replace(/\s+/g, ' ');
|
|
||||||
return (elementText: ElementText) => {
|
|
||||||
if (!text && !elementText.immediate.length)
|
|
||||||
return true;
|
|
||||||
return elementText.immediate.some(s => s.trim().replace(/\s+/g, ' ') === text);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createStrictFullTextMatcher(text: string): TextMatcher {
|
|
||||||
text = text.trim().replace(/\s+/g, ' ');
|
|
||||||
return (elementText: ElementText) => {
|
|
||||||
return elementText.full.trim().replace(/\s+/g, ' ') === text;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRegexTextMatcher(source: string, flags?: string): TextMatcher {
|
|
||||||
const re = new RegExp(source, flags);
|
|
||||||
return (elementText: ElementText) => {
|
|
||||||
return re.test(elementText.full);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shouldSkipForTextMatching(element: Element | ShadowRoot) {
|
export function shouldSkipForTextMatching(element: Element | ShadowRoot) {
|
||||||
return element.nodeName === 'SCRIPT' || element.nodeName === 'NOSCRIPT' || element.nodeName === 'STYLE' || document.head && document.head.contains(element);
|
return element.nodeName === 'SCRIPT' || element.nodeName === 'NOSCRIPT' || element.nodeName === 'STYLE' || document.head && document.head.contains(element);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ function cssEscapeOne(s: string, i: number): string {
|
||||||
return '\\' + s.charAt(i);
|
return '\\' + s.charAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeWhiteSpace(text: string): string {
|
||||||
|
return text.replace(/\u200b/g, '').trim().replace(/\s+/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
export function escapeForTextSelector(text: string | RegExp, exact: boolean): string {
|
export function escapeForTextSelector(text: string | RegExp, exact: boolean): string {
|
||||||
if (typeof text !== 'string')
|
if (typeof text !== 'string')
|
||||||
return String(text);
|
return String(text);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue