chore: unify v1 and v2 selector handling (#7844)
This commit is contained in:
parent
6b774922f9
commit
95001fe8d1
|
|
@ -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('=');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue