2020-01-06 18:22:35 -08:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
2019-11-21 14:43:30 -08:00
|
|
|
|
2019-11-22 16:21:30 -08:00
|
|
|
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
2019-11-22 15:36:17 -08:00
|
|
|
import { Utils } from './utils';
|
2019-12-16 20:49:18 -08:00
|
|
|
import { CSSEngine } from './cssSelectorEngine';
|
|
|
|
import { XPathEngine } from './xpathSelectorEngine';
|
2020-01-13 17:39:43 -08:00
|
|
|
import { TextEngine } from './textSelectorEngine';
|
2019-12-16 20:49:18 -08:00
|
|
|
|
|
|
|
function createAttributeEngine(attribute: string): SelectorEngine {
|
|
|
|
const engine: SelectorEngine = {
|
|
|
|
name: attribute,
|
|
|
|
|
|
|
|
create(root: SelectorRoot, target: Element): string | undefined {
|
|
|
|
const value = target.getAttribute(attribute);
|
|
|
|
if (!value)
|
|
|
|
return;
|
|
|
|
if (root.querySelector(`[${attribute}=${value}]`) === target)
|
|
|
|
return value;
|
|
|
|
},
|
|
|
|
|
|
|
|
query(root: SelectorRoot, selector: string): Element | undefined {
|
|
|
|
return root.querySelector(`[${attribute}=${selector}]`) || undefined;
|
|
|
|
},
|
|
|
|
|
|
|
|
queryAll(root: SelectorRoot, selector: string): Element[] {
|
|
|
|
return Array.from(root.querySelectorAll(`[${attribute}=${selector}]`));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return engine;
|
|
|
|
}
|
2019-11-22 15:36:17 -08:00
|
|
|
|
|
|
|
type ParsedSelector = { engine: SelectorEngine, selector: string }[];
|
2019-12-18 18:11:02 -08:00
|
|
|
type Predicate = (element: Element | undefined) => any;
|
2019-11-22 15:36:17 -08:00
|
|
|
|
2019-11-22 17:27:09 -08:00
|
|
|
class Injected {
|
2019-11-22 15:36:17 -08:00
|
|
|
readonly utils: Utils;
|
|
|
|
readonly engines: Map<string, SelectorEngine>;
|
|
|
|
|
2019-12-16 20:49:18 -08:00
|
|
|
constructor(customEngines: SelectorEngine[]) {
|
|
|
|
const defaultEngines = [
|
|
|
|
CSSEngine,
|
|
|
|
XPathEngine,
|
2020-01-13 17:39:43 -08:00
|
|
|
TextEngine,
|
2019-12-16 20:49:18 -08:00
|
|
|
createAttributeEngine('id'),
|
|
|
|
createAttributeEngine('data-testid'),
|
|
|
|
createAttributeEngine('data-test-id'),
|
|
|
|
createAttributeEngine('data-test'),
|
|
|
|
];
|
2019-11-22 15:36:17 -08:00
|
|
|
this.utils = new Utils();
|
|
|
|
this.engines = new Map();
|
2019-12-16 20:49:18 -08:00
|
|
|
for (const engine of [...defaultEngines, ...customEngines])
|
2019-11-22 15:36:17 -08:00
|
|
|
this.engines.set(engine.name, engine);
|
2019-11-21 14:43:30 -08:00
|
|
|
}
|
|
|
|
|
2019-12-05 16:26:09 -08:00
|
|
|
querySelector(selector: string, root: Node): Element | undefined {
|
2019-11-22 15:36:17 -08:00
|
|
|
const parsed = this._parseSelector(selector);
|
2019-12-10 11:15:14 -08:00
|
|
|
if (!(root as any)['querySelector'])
|
2019-12-05 16:26:09 -08:00
|
|
|
throw new Error('Node is not queryable.');
|
|
|
|
let element = root as SelectorRoot;
|
2019-11-22 15:36:17 -08:00
|
|
|
for (const { engine, selector } of parsed) {
|
2019-11-22 16:21:30 -08:00
|
|
|
const next = engine.query((element as Element).shadowRoot || element, selector);
|
2019-11-22 15:36:17 -08:00
|
|
|
if (!next)
|
|
|
|
return;
|
|
|
|
element = next;
|
2019-11-21 14:43:30 -08:00
|
|
|
}
|
2019-11-22 16:21:30 -08:00
|
|
|
return element as Element;
|
2019-11-21 14:43:30 -08:00
|
|
|
}
|
2019-11-22 15:36:17 -08:00
|
|
|
|
2019-12-05 16:26:09 -08:00
|
|
|
querySelectorAll(selector: string, root: Node): Element[] {
|
2019-11-22 15:36:17 -08:00
|
|
|
const parsed = this._parseSelector(selector);
|
2019-12-10 11:15:14 -08:00
|
|
|
if (!(root as any)['querySelectorAll'])
|
2019-12-05 16:26:09 -08:00
|
|
|
throw new Error('Node is not queryable.');
|
|
|
|
let set = new Set<SelectorRoot>([ root as SelectorRoot ]);
|
2019-11-22 15:36:17 -08:00
|
|
|
for (const { engine, selector } of parsed) {
|
|
|
|
const newSet = new Set<Element>();
|
|
|
|
for (const prev of set) {
|
2019-11-22 16:21:30 -08:00
|
|
|
for (const next of engine.queryAll((prev as Element).shadowRoot || prev, selector)) {
|
2019-11-22 15:36:17 -08:00
|
|
|
if (newSet.has(next))
|
|
|
|
continue;
|
|
|
|
newSet.add(next);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
set = newSet;
|
|
|
|
}
|
2019-11-22 16:21:30 -08:00
|
|
|
return Array.from(set) as Element[];
|
2019-11-22 15:36:17 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private _parseSelector(selector: string): ParsedSelector {
|
|
|
|
let index = 0;
|
|
|
|
let quote: string | undefined;
|
|
|
|
let start = 0;
|
|
|
|
const result: ParsedSelector = [];
|
|
|
|
const append = () => {
|
|
|
|
const part = selector.substring(start, index);
|
|
|
|
const eqIndex = part.indexOf('=');
|
|
|
|
if (eqIndex === -1)
|
|
|
|
throw new Error(`Cannot parse selector ${selector}`);
|
|
|
|
const name = part.substring(0, eqIndex).trim();
|
|
|
|
const body = part.substring(eqIndex + 1);
|
|
|
|
const engine = this.engines.get(name.toLowerCase());
|
|
|
|
if (!engine)
|
|
|
|
throw new Error(`Unknown engine ${name} while parsing selector ${selector}`);
|
|
|
|
result.push({ engine, selector: body });
|
|
|
|
};
|
|
|
|
while (index < selector.length) {
|
|
|
|
const c = selector[index];
|
|
|
|
if (c === '\\' && index + 1 < selector.length) {
|
|
|
|
index += 2;
|
|
|
|
} else if (c === quote) {
|
|
|
|
quote = undefined;
|
|
|
|
index++;
|
|
|
|
} else if (!quote && c === '>' && selector[index + 1] === '>') {
|
|
|
|
append();
|
|
|
|
index += 2;
|
|
|
|
start = index;
|
|
|
|
} else {
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
append();
|
|
|
|
return result;
|
|
|
|
}
|
2019-12-03 10:51:41 -08:00
|
|
|
|
2019-12-04 13:11:10 -08:00
|
|
|
isVisible(element: Element): boolean {
|
|
|
|
if (!element.ownerDocument || !element.ownerDocument.defaultView)
|
|
|
|
return true;
|
|
|
|
const style = element.ownerDocument.defaultView.getComputedStyle(element);
|
|
|
|
if (!style || style.visibility === 'hidden')
|
|
|
|
return false;
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
return !!(rect.top || rect.bottom || rect.width || rect.height);
|
|
|
|
}
|
|
|
|
|
2020-02-14 13:05:23 -08:00
|
|
|
pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> {
|
2019-12-03 10:51:41 -08:00
|
|
|
let timedOut = false;
|
|
|
|
if (timeout)
|
|
|
|
setTimeout(() => timedOut = true, timeout);
|
|
|
|
|
2019-12-18 18:11:02 -08:00
|
|
|
const element = selector === undefined ? undefined : this.querySelector(selector, document);
|
|
|
|
const success = predicate(element);
|
2019-12-03 10:51:41 -08:00
|
|
|
if (success)
|
|
|
|
return Promise.resolve(success);
|
|
|
|
|
2019-12-10 11:15:14 -08:00
|
|
|
let fulfill: (result?: any) => void;
|
2019-12-03 10:51:41 -08:00
|
|
|
const result = new Promise(x => fulfill = x);
|
2019-12-18 18:11:02 -08:00
|
|
|
const observer = new MutationObserver(() => {
|
2019-12-03 10:51:41 -08:00
|
|
|
if (timedOut) {
|
|
|
|
observer.disconnect();
|
|
|
|
fulfill();
|
2019-12-18 18:11:02 -08:00
|
|
|
return;
|
2019-12-03 10:51:41 -08:00
|
|
|
}
|
2019-12-18 18:11:02 -08:00
|
|
|
const element = selector === undefined ? undefined : this.querySelector(selector, document);
|
|
|
|
const success = predicate(element);
|
2019-12-03 10:51:41 -08:00
|
|
|
if (success) {
|
|
|
|
observer.disconnect();
|
|
|
|
fulfill(success);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
observer.observe(document, {
|
|
|
|
childList: true,
|
|
|
|
subtree: true,
|
|
|
|
attributes: true
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-02-14 13:05:23 -08:00
|
|
|
pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> {
|
2019-12-03 10:51:41 -08:00
|
|
|
let timedOut = false;
|
|
|
|
if (timeout)
|
|
|
|
setTimeout(() => timedOut = true, timeout);
|
|
|
|
|
2019-12-10 11:15:14 -08:00
|
|
|
let fulfill: (result?: any) => void;
|
2019-12-03 10:51:41 -08:00
|
|
|
const result = new Promise(x => fulfill = x);
|
|
|
|
|
2019-12-18 18:11:02 -08:00
|
|
|
const onRaf = () => {
|
2019-12-03 10:51:41 -08:00
|
|
|
if (timedOut) {
|
|
|
|
fulfill();
|
|
|
|
return;
|
|
|
|
}
|
2019-12-18 18:11:02 -08:00
|
|
|
const element = selector === undefined ? undefined : this.querySelector(selector, document);
|
|
|
|
const success = predicate(element);
|
2019-12-03 10:51:41 -08:00
|
|
|
if (success)
|
|
|
|
fulfill(success);
|
|
|
|
else
|
|
|
|
requestAnimationFrame(onRaf);
|
2019-12-18 18:11:02 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
onRaf();
|
|
|
|
return result;
|
2019-12-03 10:51:41 -08:00
|
|
|
}
|
|
|
|
|
2020-02-14 13:05:23 -08:00
|
|
|
pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise<any> {
|
2019-12-03 10:51:41 -08:00
|
|
|
let timedOut = false;
|
|
|
|
if (timeout)
|
|
|
|
setTimeout(() => timedOut = true, timeout);
|
|
|
|
|
2019-12-10 11:15:14 -08:00
|
|
|
let fulfill: (result?: any) => void;
|
2019-12-03 10:51:41 -08:00
|
|
|
const result = new Promise(x => fulfill = x);
|
2019-12-18 18:11:02 -08:00
|
|
|
const onTimeout = () => {
|
2019-12-03 10:51:41 -08:00
|
|
|
if (timedOut) {
|
|
|
|
fulfill();
|
|
|
|
return;
|
|
|
|
}
|
2019-12-18 18:11:02 -08:00
|
|
|
const element = selector === undefined ? undefined : this.querySelector(selector, document);
|
|
|
|
const success = predicate(element);
|
2019-12-03 10:51:41 -08:00
|
|
|
if (success)
|
|
|
|
fulfill(success);
|
|
|
|
else
|
|
|
|
setTimeout(onTimeout, pollInterval);
|
2019-12-18 18:11:02 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
onTimeout();
|
|
|
|
return result;
|
2019-12-03 10:51:41 -08:00
|
|
|
}
|
2019-11-21 14:43:30 -08:00
|
|
|
}
|
|
|
|
|
2019-11-22 15:36:17 -08:00
|
|
|
export default Injected;
|