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 = { export type ParsedSelectorPart = {
name: string, name: string,
body: string, body: string | CSSComplexSelectorList,
} | CSSComplexSelectorList; };
export type ParsedSelector = { export type ParsedSelector = {
parts: ParsedSelectorPart[], parts: ParsedSelectorPart[],
capture?: number, 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 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 { export function parseSelector(selector: string): ParsedSelector {
const result = parseSelectorV1(selector); const result = parseSelectorString(selector);
result.parts = result.parts.map(part => { const parts: ParsedSelectorPart[] = result.parts.map(part => {
if (Array.isArray(part))
return part;
if (part.name === 'css' || part.name === 'css:light') { if (part.name === 'css' || part.name === 'css:light') {
if (part.name === 'css:light') if (part.name === 'css:light')
part.body = ':light(' + part.body + ')'; part.body = ':light(' + part.body + ')';
const parsedCSS = parseCSS(part.body, customCSSNames); const parsedCSS = parseCSS(part.body, customCSSNames);
return parsedCSS.selector; return {
name: 'css',
body: parsedCSS.selector
};
} }
return part; return part;
}); });
return { return {
parts: result.parts,
capture: result.capture, capture: result.capture,
parts
}; };
} }
function parseSelectorV1(selector: string): ParsedSelector { function parseSelectorString(selector: string): ParsedSelectorStrings {
let index = 0; let index = 0;
let quote: string | undefined; let quote: string | undefined;
let start = 0; let start = 0;
const result: ParsedSelector = { parts: [] }; const result: ParsedSelectorStrings = { parts: [] };
const append = () => { const append = () => {
const part = selector.substring(start, index).trim(); const part = selector.substring(start, index).trim();
const eqIndex = part.indexOf('='); 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 ElementStateWithoutStable = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked';
export type ElementState = ElementStateWithoutStable | 'stable'; export type ElementState = ElementStateWithoutStable | 'stable';
export interface SelectorEngineV2 {
query?(root: SelectorRoot, body: any): Element | undefined;
queryAll(root: SelectorRoot, body: any): Element[];
}
export class InjectedScript { export class InjectedScript {
private _enginesV1: Map<string, SelectorEngine>; private _engines: Map<string, SelectorEngineV2>;
_evaluator: SelectorEvaluatorImpl; _evaluator: SelectorEvaluatorImpl;
private _stableRafCount: number; private _stableRafCount: number;
private _replaceRafWithTimeout: boolean; private _replaceRafWithTimeout: boolean;
constructor(stableRafCount: number, replaceRafWithTimeout: boolean, customEngines: { name: string, engine: SelectorEngine}[]) { 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._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._stableRafCount = stableRafCount;
this._replaceRafWithTimeout = replaceRafWithTimeout; this._replaceRafWithTimeout = replaceRafWithTimeout;
} }
@ -73,7 +79,7 @@ export class InjectedScript {
parseSelector(selector: string): ParsedSelector { parseSelector(selector: string): ParsedSelector {
const result = parseSelector(selector); const result = parseSelector(selector);
for (const part of result.parts) { 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}`); throw new Error(`Unknown engine "${part.name}" while parsing selector ${selector}`);
} }
return result; return result;
@ -136,15 +142,15 @@ export class InjectedScript {
} }
private _queryEngine(part: ParsedSelectorPart, root: SelectorRoot): Element | undefined { private _queryEngine(part: ParsedSelectorPart, root: SelectorRoot): Element | undefined {
if (Array.isArray(part)) const engine = this._engines.get(part.name)!;
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, part)[0]; if (engine.query)
return this._enginesV1.get(part.name)!.query(root, part.body); return engine.query(root, part.body);
else
return engine.queryAll(root, part.body)[0];
} }
private _queryEngineAll(part: ParsedSelectorPart, root: SelectorRoot): Element[] { private _queryEngineAll(part: ParsedSelectorPart, root: SelectorRoot): Element[] {
if (Array.isArray(part)) return this._engines.get(part.name)!.queryAll(root, part.body);
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, part);
return this._enginesV1.get(part.name)!.queryAll(root, part.body);
} }
private _createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine { private _createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine {
@ -153,22 +159,28 @@ export class InjectedScript {
return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }]; return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }];
}; };
return { 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[] => { queryAll: (root: SelectorRoot, selector: string): Element[] => {
return this._evaluator.query({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector)); 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 { private _createTextEngine(shadow: boolean): SelectorEngine {
const queryList = (root: SelectorRoot, selector: string, single: boolean): Element[] => { const queryList = (root: SelectorRoot, selector: string, single: boolean): Element[] => {
const { matcher, kind } = createTextMatcher(selector); const { matcher, kind } = createTextMatcher(selector);
const result: Element[] = []; const result: Element[] = [];
let lastDidNotMatchSelf: Element | null = null; let lastDidNotMatchSelf: Element | null = null;
const checkElement = (element: Element) => { const appendElement = (element: Element) => {
// TODO: replace contains() with something shadow-dom-aware? // TODO: replace contains() with something shadow-dom-aware?
if (kind === 'lax' && lastDidNotMatchSelf && lastDidNotMatchSelf.contains(element)) if (kind === 'lax' && lastDidNotMatchSelf && lastDidNotMatchSelf.contains(element))
return false; return false;
@ -180,11 +192,11 @@ export class InjectedScript {
return single && result.length > 0; 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; return result;
const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: shadow }, '*'); const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: shadow }, '*');
for (const element of elements) { for (const element of elements) {
if (checkElement(element)) if (appendElement(element))
return result; return result;
} }
return result; return result;
@ -194,6 +206,7 @@ export class InjectedScript {
query: (root: SelectorRoot, selector: string): Element | undefined => { query: (root: SelectorRoot, selector: string): Element | undefined => {
return queryList(root, selector, true)[0]; return queryList(root, selector, true)[0];
}, },
queryAll: (root: SelectorRoot, selector: string): Element[] => { queryAll: (root: SelectorRoot, selector: string): Element[] => {
return queryList(root, selector, false); return queryList(root, selector, false);
} }

View file

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