chore: unify v1 and v2 selector handling (#7844)

This commit is contained in:
Pavel Feldman 2021-07-26 15:07:12 -07:00 committed by GitHub
parent 6b774922f9
commit 95001fe8d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 42 deletions

View File

@ -18,40 +18,46 @@ import { CSSComplexSelectorList, parseCSS } from './cssParser';
export type ParsedSelectorPart = {
name: string,
body: string,
} | CSSComplexSelectorList;
body: string | CSSComplexSelectorList,
};
export type ParsedSelector = {
parts: ParsedSelectorPart[],
capture?: number,
};
type ParsedSelectorStrings = {
parts: { name: string, body: string }[],
capture?: number,
};
export const customCSSNames = new Set(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']);
export function parseSelector(selector: string): ParsedSelector {
const result = parseSelectorV1(selector);
result.parts = result.parts.map(part => {
if (Array.isArray(part))
return part;
const result = parseSelectorString(selector);
const parts: ParsedSelectorPart[] = result.parts.map(part => {
if (part.name === 'css' || part.name === 'css:light') {
if (part.name === 'css:light')
part.body = ':light(' + part.body + ')';
const parsedCSS = parseCSS(part.body, customCSSNames);
return parsedCSS.selector;
return {
name: 'css',
body: parsedCSS.selector
};
}
return part;
});
return {
parts: result.parts,
capture: result.capture,
parts
};
}
function parseSelectorV1(selector: string): ParsedSelector {
function parseSelectorString(selector: string): ParsedSelectorStrings {
let index = 0;
let quote: string | undefined;
let start = 0;
const result: ParsedSelector = { parts: [] };
const result: ParsedSelectorStrings = { parts: [] };
const append = () => {
const part = selector.substring(start, index).trim();
const eqIndex = part.indexOf('=');

View File

@ -41,31 +41,37 @@ export type InjectedScriptPoll<T> = {
export type ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked';
export type ElementState = ElementStateWithoutStable | 'stable';
export interface SelectorEngineV2 {
query?(root: SelectorRoot, body: any): Element | undefined;
queryAll(root: SelectorRoot, body: any): Element[];
}
export class InjectedScript {
private _enginesV1: Map<string, SelectorEngine>;
private _engines: Map<string, SelectorEngineV2>;
_evaluator: SelectorEvaluatorImpl;
private _stableRafCount: number;
private _replaceRafWithTimeout: boolean;
constructor(stableRafCount: number, replaceRafWithTimeout: boolean, customEngines: { name: string, engine: SelectorEngine}[]) {
this._enginesV1 = new Map();
this._enginesV1.set('xpath', XPathEngine);
this._enginesV1.set('xpath:light', XPathEngine);
this._enginesV1.set('text', this._createTextEngine(true));
this._enginesV1.set('text:light', this._createTextEngine(false));
this._enginesV1.set('id', this._createAttributeEngine('id', true));
this._enginesV1.set('id:light', this._createAttributeEngine('id', false));
this._enginesV1.set('data-testid', this._createAttributeEngine('data-testid', true));
this._enginesV1.set('data-testid:light', this._createAttributeEngine('data-testid', false));
this._enginesV1.set('data-test-id', this._createAttributeEngine('data-test-id', true));
this._enginesV1.set('data-test-id:light', this._createAttributeEngine('data-test-id', false));
this._enginesV1.set('data-test', this._createAttributeEngine('data-test', true));
this._enginesV1.set('data-test:light', this._createAttributeEngine('data-test', false));
for (const { name, engine } of customEngines)
this._enginesV1.set(name, engine);
// No custom engines in V2 for now.
this._evaluator = new SelectorEvaluatorImpl(new Map());
this._engines = new Map();
this._engines.set('xpath', XPathEngine);
this._engines.set('xpath:light', XPathEngine);
this._engines.set('text', this._createTextEngine(true));
this._engines.set('text:light', this._createTextEngine(false));
this._engines.set('id', this._createAttributeEngine('id', true));
this._engines.set('id:light', this._createAttributeEngine('id', false));
this._engines.set('data-testid', this._createAttributeEngine('data-testid', true));
this._engines.set('data-testid:light', this._createAttributeEngine('data-testid', false));
this._engines.set('data-test-id', this._createAttributeEngine('data-test-id', true));
this._engines.set('data-test-id:light', this._createAttributeEngine('data-test-id', false));
this._engines.set('data-test', this._createAttributeEngine('data-test', true));
this._engines.set('data-test:light', this._createAttributeEngine('data-test', false));
this._engines.set('css', this._createCSSEngine());
for (const { name, engine } of customEngines)
this._engines.set(name, engine);
this._stableRafCount = stableRafCount;
this._replaceRafWithTimeout = replaceRafWithTimeout;
}
@ -73,7 +79,7 @@ export class InjectedScript {
parseSelector(selector: string): ParsedSelector {
const result = parseSelector(selector);
for (const part of result.parts) {
if (!Array.isArray(part) && !this._enginesV1.has(part.name))
if (!this._engines.has(part.name))
throw new Error(`Unknown engine "${part.name}" while parsing selector ${selector}`);
}
return result;
@ -136,15 +142,15 @@ export class InjectedScript {
}
private _queryEngine(part: ParsedSelectorPart, root: SelectorRoot): Element | undefined {
if (Array.isArray(part))
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, part)[0];
return this._enginesV1.get(part.name)!.query(root, part.body);
const engine = this._engines.get(part.name)!;
if (engine.query)
return engine.query(root, part.body);
else
return engine.queryAll(root, part.body)[0];
}
private _queryEngineAll(part: ParsedSelectorPart, root: SelectorRoot): Element[] {
if (Array.isArray(part))
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, part);
return this._enginesV1.get(part.name)!.queryAll(root, part.body);
return this._engines.get(part.name)!.queryAll(root, part.body);
}
private _createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine {
@ -153,22 +159,28 @@ export class InjectedScript {
return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }];
};
return {
query: (root: SelectorRoot, selector: string): Element | undefined => {
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector))[0];
},
queryAll: (root: SelectorRoot, selector: string): Element[] => {
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector));
}
};
}
private _createCSSEngine(): SelectorEngineV2 {
const evaluator = this._evaluator;
return {
queryAll(root: SelectorRoot, body: any) {
return evaluator.query({ scope: root as Document | Element, pierceShadow: true }, body);
}
};
}
private _createTextEngine(shadow: boolean): SelectorEngine {
const queryList = (root: SelectorRoot, selector: string, single: boolean): Element[] => {
const { matcher, kind } = createTextMatcher(selector);
const result: Element[] = [];
let lastDidNotMatchSelf: Element | null = null;
const checkElement = (element: Element) => {
const appendElement = (element: Element) => {
// TODO: replace contains() with something shadow-dom-aware?
if (kind === 'lax' && lastDidNotMatchSelf && lastDidNotMatchSelf.contains(element))
return false;
@ -180,11 +192,11 @@ export class InjectedScript {
return single && result.length > 0;
};
if (root.nodeType === Node.ELEMENT_NODE && checkElement(root as Element))
if (root.nodeType === Node.ELEMENT_NODE && appendElement(root as Element))
return result;
const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: shadow }, '*');
for (const element of elements) {
if (checkElement(element))
if (appendElement(element))
return result;
}
return result;
@ -194,6 +206,7 @@ export class InjectedScript {
query: (root: SelectorRoot, selector: string): Element | undefined => {
return queryList(root, selector, true)[0];
},
queryAll: (root: SelectorRoot, selector: string): Element[] => {
return queryList(root, selector, false);
}

View File

@ -17,6 +17,7 @@
export type SelectorRoot = Element | ShadowRoot | Document;
export interface SelectorEngine {
query(root: SelectorRoot, selector: string): Element | undefined;
query?(root: SelectorRoot, selector: string): Element | undefined;
queryAll(root: SelectorRoot, selector: string): Element[];
}