feat: introduce vue selector engine (#8070)
This patch adds support for the `vue` selector engine that allows selecting DOM elements based on the component name. > **NOTE**: `vue` engine supports Vue2 and Vue2. References #7189
This commit is contained in:
parent
290f601dae
commit
f455b6edc0
|
|
@ -196,6 +196,23 @@ methods accept [`param: selector`] as their first argument.
|
||||||
await page.ClickAsync("react=DatePickerComponent");
|
await page.ClickAsync("react=DatePickerComponent");
|
||||||
```
|
```
|
||||||
Learn more about [React selector][react].
|
Learn more about [React selector][react].
|
||||||
|
- Vue selector
|
||||||
|
```js
|
||||||
|
await page.click('vue=DatePickerComponent');
|
||||||
|
```
|
||||||
|
```java
|
||||||
|
page.click("vue=DatePickerComponent");
|
||||||
|
```
|
||||||
|
```python async
|
||||||
|
await page.click("vue=DatePickerComponent")
|
||||||
|
```
|
||||||
|
```python sync
|
||||||
|
page.click("vue=DatePickerComponent")
|
||||||
|
```
|
||||||
|
```csharp
|
||||||
|
await page.ClickAsync("vue=DatePickerComponent");
|
||||||
|
```
|
||||||
|
Learn more about [Vue selector][vue].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -660,6 +677,22 @@ React selectors support React 15 and above.
|
||||||
React selectors, as well as [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi), only work against **unminified** application builds.
|
React selectors, as well as [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi), only work against **unminified** application builds.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## Vue selectors
|
||||||
|
|
||||||
|
Vue selectors allow selecting elements by its component name.
|
||||||
|
|
||||||
|
To find Vue element names in a tree use [Vue DevTools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en).
|
||||||
|
|
||||||
|
Example: `vue=MyComponent`
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Vue selectors support Vue2 and above.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Vue selectors, as well as [Vue DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi), only work against **unminified** application builds.
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
## id, data-testid, data-test-id, data-test selectors
|
## id, data-testid, data-test-id, data-test selectors
|
||||||
|
|
||||||
|
|
@ -1003,4 +1036,5 @@ await page.ClickAsync("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input");
|
||||||
[css]: #css-selector
|
[css]: #css-selector
|
||||||
[xpath]: #xpath-selectors
|
[xpath]: #xpath-selectors
|
||||||
[react]: #react-selectors
|
[react]: #react-selectors
|
||||||
|
[vue]: #vue-selectors
|
||||||
[id]: #id-data-testid-data-test-id-data-test-selectors
|
[id]: #id-data-testid-data-test-id-data-test-selectors
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||||
import { XPathEngine } from './xpathSelectorEngine';
|
import { XPathEngine } from './xpathSelectorEngine';
|
||||||
import { ReactEngine } from './reactSelectorEngine';
|
import { ReactEngine } from './reactSelectorEngine';
|
||||||
|
import { VueEngine } from './vueSelectorEngine';
|
||||||
import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/selectorParser';
|
import { ParsedSelector, ParsedSelectorPart, parseSelector } from '../common/selectorParser';
|
||||||
import { FatalDOMError } from '../common/domErrors';
|
import { FatalDOMError } from '../common/domErrors';
|
||||||
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
||||||
|
|
@ -64,6 +65,7 @@ export class InjectedScript {
|
||||||
this._engines.set('xpath', XPathEngine);
|
this._engines.set('xpath', XPathEngine);
|
||||||
this._engines.set('xpath:light', XPathEngine);
|
this._engines.set('xpath:light', XPathEngine);
|
||||||
this._engines.set('react', ReactEngine);
|
this._engines.set('react', ReactEngine);
|
||||||
|
this._engines.set('vue', VueEngine);
|
||||||
this._engines.set('text', this._createTextEngine(true));
|
this._engines.set('text', this._createTextEngine(true));
|
||||||
this._engines.set('text:light', this._createTextEngine(false));
|
this._engines.set('text:light', this._createTextEngine(false));
|
||||||
this._engines.set('id', this._createAttributeEngine('id', true));
|
this._engines.set('id', this._createAttributeEngine('id', true));
|
||||||
|
|
|
||||||
234
src/server/injected/vueSelectorEngine.ts
Normal file
234
src/server/injected/vueSelectorEngine.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
type ComponentNode = {
|
||||||
|
name: string,
|
||||||
|
children: ComponentNode[],
|
||||||
|
rootElements: Element[],
|
||||||
|
};
|
||||||
|
|
||||||
|
type VueVNode = {
|
||||||
|
// Vue3
|
||||||
|
type: any,
|
||||||
|
root?: any,
|
||||||
|
parent?: VueVNode,
|
||||||
|
appContext?: any,
|
||||||
|
_isBeingDestroyed?: any,
|
||||||
|
isUnmounted?: any,
|
||||||
|
subTree: any,
|
||||||
|
|
||||||
|
// Vue2
|
||||||
|
$children?: VueVNode[],
|
||||||
|
fnOptions?: any,
|
||||||
|
$options?: any,
|
||||||
|
$root?: VueVNode,
|
||||||
|
$el?: Element,
|
||||||
|
};
|
||||||
|
|
||||||
|
// @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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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!],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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, name: string): Element[] {
|
||||||
|
const vueRoot = findVueRoot();
|
||||||
|
if (!vueRoot)
|
||||||
|
return [];
|
||||||
|
const tree = vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root);
|
||||||
|
const treeNodes = filterComponentsTree(tree, treeNode => {
|
||||||
|
if (treeNode.name !== name)
|
||||||
|
return false;
|
||||||
|
if (treeNode.rootElements.some(rootElement => !scope.contains(rootElement)))
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -39,7 +39,7 @@ export class Selectors {
|
||||||
this._builtinEngines = new Set([
|
this._builtinEngines = new Set([
|
||||||
'css', 'css:light',
|
'css', 'css:light',
|
||||||
'xpath', 'xpath:light',
|
'xpath', 'xpath:light',
|
||||||
'react',
|
'react', 'vue',
|
||||||
'text', 'text:light',
|
'text', 'text:light',
|
||||||
'id', 'id:light',
|
'id', 'id:light',
|
||||||
'data-testid', 'data-testid:light',
|
'data-testid', 'data-testid:light',
|
||||||
|
|
@ -48,7 +48,7 @@ export class Selectors {
|
||||||
'_visible', '_nth'
|
'_visible', '_nth'
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
'react',
|
'react', 'vue',
|
||||||
]);
|
]);
|
||||||
this._engines = new Map();
|
this._engines = new Map();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
tests/assets/reading-list/vue2.html
Normal file
87
tests/assets/reading-list/vue2.html
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<link rel=stylesheet href='./style.css'>
|
||||||
|
<script src="./vue_2.6.14.js"></script>
|
||||||
|
|
||||||
|
<div id=root></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
Vue.component('app-header', {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<h1>vuejs@${Vue.version}</h1>
|
||||||
|
<h3>Reading List: {{ bookCount }}</h3>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
props: [ 'bookCount' ],
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('new-book', {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<input v-model='name' @keypress.enter='onNewBook'>
|
||||||
|
<button @click='onNewBook'>new book</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
emits: ['newbook'],
|
||||||
|
methods: {
|
||||||
|
onNewBook() {
|
||||||
|
this.$emit('newbook', this.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Vue.component('book-item', {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
props: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.component('book-list', {
|
||||||
|
props: ['books'],
|
||||||
|
template: `
|
||||||
|
<ol>
|
||||||
|
<li v-for='book in books' :key='book.name'>
|
||||||
|
<book-item :name='book.name'></book-item>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#root',
|
||||||
|
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<app-header :bookCount='books.length'></app-header>
|
||||||
|
<new-book @newbook='addNewBook'></new-book>
|
||||||
|
<book-list :books='books'></book-list>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
books: [
|
||||||
|
{name: 'Pride and Prejudice' },
|
||||||
|
{name: 'To Kill a Mockingbird' },
|
||||||
|
{name: 'The Great Gatsby' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addNewBook(name) {
|
||||||
|
console.log('here');
|
||||||
|
this.books.push({name});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
79
tests/assets/reading-list/vue3.html
Normal file
79
tests/assets/reading-list/vue3.html
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<link rel=stylesheet href='./style.css'>
|
||||||
|
<script src="./vue_3.1.5.js"></script>
|
||||||
|
|
||||||
|
<div id=root></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const app = Vue.createApp({
|
||||||
|
template: `
|
||||||
|
<app-header :bookCount='books.length'></app-header>
|
||||||
|
<new-book @newbook='addNewBook'></new-book>
|
||||||
|
<book-list :books='books'></book-list>
|
||||||
|
`,
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
books: [
|
||||||
|
{name: 'Pride and Prejudice' },
|
||||||
|
{name: 'To Kill a Mockingbird' },
|
||||||
|
{name: 'The Great Gatsby' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
addNewBook(name) {
|
||||||
|
console.log('here');
|
||||||
|
this.books.push({name});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.component('app-header', {
|
||||||
|
template: `
|
||||||
|
<h1>vuejs@${Vue.version}</h1>
|
||||||
|
<h3>Reading List: {{ bookCount }}</h3>
|
||||||
|
`,
|
||||||
|
props: [ 'bookCount' ],
|
||||||
|
});
|
||||||
|
|
||||||
|
app.component('new-book', {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<input v-model='name' @keypress.enter='onNewBook'><button @click='onNewBook'>new book</button>
|
||||||
|
`,
|
||||||
|
emits: ['newbook'],
|
||||||
|
methods: {
|
||||||
|
onNewBook() {
|
||||||
|
this.$emit('newbook', this.name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app.component('book-item', {
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
props: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
app.component('book-list', {
|
||||||
|
props: ['books'],
|
||||||
|
template: `
|
||||||
|
<ol>
|
||||||
|
<li v-for='book in books' :key='book.name'>
|
||||||
|
<book-item :name='book.name'></book-item>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount('#root');
|
||||||
|
|
||||||
|
</script>
|
||||||
12014
tests/assets/reading-list/vue_2.6.14.js
Normal file
12014
tests/assets/reading-list/vue_2.6.14.js
Normal file
File diff suppressed because it is too large
Load diff
14997
tests/assets/reading-list/vue_3.1.5.js
Normal file
14997
tests/assets/reading-list/vue_3.1.5.js
Normal file
File diff suppressed because it is too large
Load diff
57
tests/page/selectors-vue.spec.ts
Normal file
57
tests/page/selectors-vue.spec.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2018 Google Inc. All rights reserved.
|
||||||
|
* Modifications 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 { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
|
const vues = {
|
||||||
|
'vue2': '/reading-list/vue2.html',
|
||||||
|
'vue3': '/reading-list/vue3.html',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [name, url] of Object.entries(vues)) {
|
||||||
|
it.describe(name, () => {
|
||||||
|
it.beforeEach(async ({page, server}) => {
|
||||||
|
await page.goto(server.PREFIX + url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with single-root elements', async ({page}) => {
|
||||||
|
expect(await page.$$eval(`vue=book-list`, els => els.length)).toBe(1);
|
||||||
|
expect(await page.$$eval(`vue=book-item`, els => els.length)).toBe(3);
|
||||||
|
expect(await page.$$eval(`vue=book-list >> vue=book-item`, els => els.length)).toBe(3);
|
||||||
|
expect(await page.$$eval(`vue=book-item >> vue=book-list`, els => els.length)).toBe(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with multi-root elements (fragments)', async ({page}) => {
|
||||||
|
it.skip(name === 'vue2', 'vue2 does not support fragments');
|
||||||
|
expect(await page.$$eval(`vue=Root`, els => els.length)).toBe(5);
|
||||||
|
expect(await page.$$eval(`vue=app-header`, els => els.length)).toBe(2);
|
||||||
|
expect(await page.$$eval(`vue=new-book`, els => els.length)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not crash when there is no match', async ({page}) => {
|
||||||
|
expect(await page.$$eval(`vue=apps`, els => els.length)).toBe(0);
|
||||||
|
expect(await page.$$eval(`vue=book-li`, els => els.length)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compose', async ({page}) => {
|
||||||
|
expect(await page.$eval(`vue=book-item >> text=Gatsby`, el => el.textContent.trim())).toBe('The Great Gatsby');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in a new issue