playwright/src/server/injected/vueSelectorEngine.ts

247 lines
9.5 KiB
TypeScript

/**
* 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.
*/
import { SelectorEngine, SelectorRoot } from './selectorEngine';
import { checkComponentAttribute, parseComponentSelector } from '../common/componentUtils';
type ComponentNode = {
name: string,
children: ComponentNode[],
rootElements: Element[],
props: any,
};
type VueVNode = {
// Vue3
type: any,
root?: any,
parent?: VueVNode,
appContext?: any,
_isBeingDestroyed?: any,
isUnmounted?: any,
subTree: any,
props: any,
// Vue2
$children?: VueVNode[],
fnOptions?: any,
$options?: any,
$root?: VueVNode,
$el?: Element,
_props: any,
};
// @see https://github.com/vuejs/devtools/blob/14085e25313bcf8ffcb55f9092a40bc0fe3ac11c/packages/shared-utils/src/util.ts#L295
function basename(filename: string, ext: string): string {
const normalized = filename.replace(/^[a-zA-Z]:/, '').replace(/\\/g, '/');
let result = normalized.substring(normalized.lastIndexOf('/') + 1);
if (ext && result.endsWith(ext))
result = result.substring(0, result.length - ext.length);
return result;
}
// @see https://github.com/vuejs/devtools/blob/14085e25313bcf8ffcb55f9092a40bc0fe3ac11c/packages/shared-utils/src/util.ts#L41
function toUpper(_: any, c: string): string {
return c ? c.toUpperCase() : '';
}
// @see https://github.com/vuejs/devtools/blob/14085e25313bcf8ffcb55f9092a40bc0fe3ac11c/packages/shared-utils/src/util.ts#L23
const classifyRE = /(?:^|[-_/])(\w)/g;
const classify = (str: string) => {
return str && str.replace(classifyRE, toUpper);
};
function buildComponentsTreeVue3(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L47
function getComponentTypeName(options: any): string|undefined {
const name = options.name || options._componentTag || options.__playwright_guessedName;
if (name)
return name;
const file = options.__file; // injected by vue-loader
if (file)
return classify(basename(file, '.vue'));
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L42
function saveComponentName(instance: VueVNode, key: string): string {
instance.type.__playwright_guessedName = key;
return key;
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L29
function getInstanceName(instance: VueVNode): string {
const name = getComponentTypeName(instance.type || {});
if (name) return name;
if (instance.root === instance) return 'Root';
for (const key in instance.parent?.type?.components)
if (instance.parent?.type.components[key] === instance.type) return saveComponentName(instance, key);
for (const key in instance.appContext?.components)
if (instance.appContext.components[key] === instance.type) return saveComponentName(instance, key);
return 'Anonymous Component';
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L6
function isBeingDestroyed(instance: VueVNode): boolean {
return instance._isBeingDestroyed || instance.isUnmounted;
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/util.ts#L16
function isFragment(instance: VueVNode): boolean {
return instance.subTree.type.toString() === 'Symbol(Fragment)';
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/tree.ts#L79
function getInternalInstanceChildren(subTree: any): VueVNode[] {
const list = [];
if (subTree.component)
list.push(subTree.component);
if (subTree.suspense)
list.push(...getInternalInstanceChildren(subTree.suspense.activeBranch));
if (Array.isArray(subTree.children)) {
subTree.children.forEach((childSubTree: any) => {
if (childSubTree.component)
list.push(childSubTree.component);
else
list.push(...getInternalInstanceChildren(childSubTree));
});
}
return list.filter(child => !isBeingDestroyed(child) && !child.type.devtools?.hide);
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/el.ts#L8
function getRootElementsFromComponentInstance(instance: VueVNode): Element[] {
if (isFragment(instance))
return getFragmentRootElements(instance.subTree);
return [instance.subTree.el];
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue3/src/components/el.ts#L15
function getFragmentRootElements(vnode: any): Element[] {
if (!vnode.children) return [];
const list = [];
for (let i = 0, l = vnode.children.length; i < l; i++) {
const childVnode = vnode.children[i];
if (childVnode.component)
list.push(...getRootElementsFromComponentInstance(childVnode.component));
else if (childVnode.el)
list.push(childVnode.el);
}
return list;
}
function buildComponentsTree(instance: VueVNode): ComponentNode {
return {
name: getInstanceName(instance),
children: getInternalInstanceChildren(instance.subTree).map(buildComponentsTree),
rootElements: getRootElementsFromComponentInstance(instance),
props: instance.props,
};
}
return buildComponentsTree(instance);
}
function buildComponentsTreeVue2(instance: VueVNode): ComponentNode {
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/shared-utils/src/util.ts#L302
function getComponentName(options: any): string|undefined {
const name = options.displayName || options.name || options._componentTag;
if (name)
return name;
const file = options.__file; // injected by vue-loader
if (file)
return classify(basename(file, '.vue'));
}
// @see https://github.com/vuejs/devtools/blob/e7132f3392b975e39e1d9a23cf30456c270099c2/packages/app-backend-vue2/src/components/util.ts#L10
function getInstanceName(instance: VueVNode): string {
const name = getComponentName(instance.$options || instance.fnOptions || {});
if (name)
return name;
return instance.$root === instance ? 'Root' : 'Anonymous Component';
}
// @see https://github.com/vuejs/devtools/blob/14085e25313bcf8ffcb55f9092a40bc0fe3ac11c/packages/app-backend-vue2/src/components/tree.ts#L103
function getInternalInstanceChildren(instance: VueVNode): VueVNode[] {
if (instance.$children)
return instance.$children;
if (Array.isArray(instance.subTree.children))
return instance.subTree.children.filter((vnode: any) => !!vnode.component).map((vnode: any) => vnode.component);
return [];
}
function buildComponentsTree(instance: VueVNode): ComponentNode {
return {
name: getInstanceName(instance),
children: getInternalInstanceChildren(instance).map(buildComponentsTree),
rootElements: [instance.$el!],
props: instance._props,
};
}
return buildComponentsTree(instance);
}
function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: ComponentNode) => boolean, result: ComponentNode[] = []): ComponentNode[] {
if (searchFn(treeNode))
result.push(treeNode);
for (const child of treeNode.children)
filterComponentsTree(child, searchFn, result);
return result;
}
function findVueRoot(): undefined|{version: number, root: VueVNode} {
const walker = document.createTreeWalker(document);
while (walker.nextNode()) {
// Vue3 root
if ((walker.currentNode as any)._vnode && (walker.currentNode as any)._vnode.component)
return {root: (walker.currentNode as any)._vnode.component, version: 3};
// Vue2 root
if ((walker.currentNode as any).__vue__)
return {root: (walker.currentNode as any).__vue__, version: 2};
}
return undefined;
}
export const VueEngine: SelectorEngine = {
queryAll(scope: SelectorRoot, selector: string): Element[] {
const {name, attributes} = parseComponentSelector(selector);
const vueRoot = findVueRoot();
if (!vueRoot)
return [];
const tree = vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root);
const treeNodes = filterComponentsTree(tree, treeNode => {
if (name && treeNode.name !== name)
return false;
if (treeNode.rootElements.some(rootElement => !scope.contains(rootElement)))
return false;
for (const attr of attributes) {
if (!checkComponentAttribute(treeNode.props, attr))
return false;
}
return true;
});
const allRootElements: Set<Element> = new Set();
for (const treeNode of treeNodes) {
for (const rootElement of treeNode.rootElements)
allRootElements.add(rootElement);
}
return [...allRootElements];
}
};