fix: support shadow DOM with Vue and React selectors (#10742)
There were two issues: - we did not find VDom roots inside shadow DOM - we incorrectly relied on DOM's `contain` method to determine if VDom's rendered node belongs to requested scope. Fixes #10123
This commit is contained in:
parent
da13d025dc
commit
a89fe3ec5c
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||
import { isInsideScope } from './selectorEvaluator';
|
||||
import { checkComponentAttribute, parseComponentSelector } from '../common/componentUtils';
|
||||
|
||||
type ComponentNode = {
|
||||
|
|
@ -132,24 +133,28 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
|||
return result;
|
||||
}
|
||||
|
||||
function findReactRoots(): ReactVNode[] {
|
||||
const roots: ReactVNode[] = [];
|
||||
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);
|
||||
while (walker.nextNode()) {
|
||||
function findReactRoots(root: Document | ShadowRoot, roots: ReactVNode[] = []): ReactVNode[] {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
do {
|
||||
const node = walker.currentNode;
|
||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L329
|
||||
if (node.hasOwnProperty('_reactRootContainer'))
|
||||
roots.push((node as any)._reactRootContainer._internalRoot.current);
|
||||
}
|
||||
// Pre-react 16: query dom for `data-reactroot`
|
||||
// @see https://github.com/facebook/react/issues/10971
|
||||
for (const node of document.querySelectorAll('[data-reactroot]')) {
|
||||
for (const key of Object.keys(node)) {
|
||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L334
|
||||
if (key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber'))
|
||||
roots.push((node as any)[key]);
|
||||
|
||||
// Pre-react 16: rely on `data-reactroot`
|
||||
// @see https://github.com/facebook/react/issues/10971
|
||||
if ((node instanceof Element) && node.hasAttribute('data-reactroot')) {
|
||||
for (const key of Object.keys(node)) {
|
||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L334
|
||||
if (key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber'))
|
||||
roots.push((node as any)[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shadowRoot = node instanceof Element ? node.shadowRoot : null;
|
||||
if (shadowRoot)
|
||||
findReactRoots(shadowRoot, roots);
|
||||
} while (walker.nextNode());
|
||||
return roots;
|
||||
}
|
||||
|
||||
|
|
@ -157,12 +162,12 @@ export const ReactEngine: SelectorEngine = {
|
|||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||
const { name, attributes } = parseComponentSelector(selector);
|
||||
|
||||
const reactRoots = findReactRoots();
|
||||
const reactRoots = findReactRoots(document);
|
||||
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
|
||||
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
|
||||
if (name && treeNode.name !== name)
|
||||
return false;
|
||||
if (treeNode.rootElements.some(domNode => !scope.contains(domNode)))
|
||||
if (treeNode.rootElements.some(domNode => !isInsideScope(scope, domNode)))
|
||||
return false;
|
||||
for (const attr of attributes) {
|
||||
if (!checkComponentAttribute(treeNode.props, attr))
|
||||
|
|
|
|||
|
|
@ -619,6 +619,17 @@ const nthMatchEngine: SelectorEngine = {
|
|||
},
|
||||
};
|
||||
|
||||
export function isInsideScope(scope: Node, element: Element | undefined): boolean {
|
||||
while (element) {
|
||||
if (scope.contains(element))
|
||||
return true;
|
||||
while (element.parentElement)
|
||||
element = element.parentElement;
|
||||
element = parentElementOrShadowHost(element);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function parentElementOrShadowHost(element: Element): Element | undefined {
|
||||
if (element.parentElement)
|
||||
return element.parentElement;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { SelectorEngine, SelectorRoot } from './selectorEngine';
|
||||
import { isInsideScope } from './selectorEvaluator';
|
||||
import { checkComponentAttribute, parseComponentSelector } from '../common/componentUtils';
|
||||
|
||||
type ComponentNode = {
|
||||
|
|
@ -205,21 +206,21 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
|||
}
|
||||
|
||||
type VueRoot = {version: number, root: VueVNode};
|
||||
function findVueRoots(): VueRoot[] {
|
||||
const roots: VueRoot[] = [];
|
||||
// Vue3 roots are marked with [data-v-app] attribute
|
||||
for (const node of document.querySelectorAll('[data-v-app]')) {
|
||||
if ((node as any)._vnode && (node as any)._vnode.component)
|
||||
roots.push({ root: (node as any)._vnode.component, version: 3 });
|
||||
}
|
||||
function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRoot[] {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
// Vue2 roots are referred to from elements.
|
||||
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);
|
||||
const vue2Roots: Set<VueVNode> = new Set();
|
||||
while (walker.nextNode()) {
|
||||
const element = walker.currentNode as any;
|
||||
if (element && element.__vue__)
|
||||
vue2Roots.add(element.__vue__.$root);
|
||||
}
|
||||
const vue2Roots: Set<VueVNode> = new Set();
|
||||
do {
|
||||
const node = walker.currentNode;
|
||||
if ((node as any).__vue__)
|
||||
vue2Roots.add((node as any).__vue__.$root);
|
||||
// Vue3 roots are marked with __vue_app__.
|
||||
if ((node as any).__vue_app__ && (node as any)._vnode && (node as any)._vnode.component)
|
||||
roots.push({ root: (node as any)._vnode.component, version: 3 });
|
||||
const shadowRoot = node instanceof Element ? node.shadowRoot : null;
|
||||
if (shadowRoot)
|
||||
findVueRoots(shadowRoot, roots);
|
||||
} while (walker.nextNode());
|
||||
for (const vue2root of vue2Roots) {
|
||||
roots.push({
|
||||
version: 2,
|
||||
|
|
@ -232,12 +233,12 @@ function findVueRoots(): VueRoot[] {
|
|||
export const VueEngine: SelectorEngine = {
|
||||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||
const { name, attributes } = parseComponentSelector(selector);
|
||||
const vueRoots = findVueRoots();
|
||||
const vueRoots = findVueRoots(document);
|
||||
const trees = vueRoots.map(vueRoot => vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root));
|
||||
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
|
||||
if (name && treeNode.name !== name)
|
||||
return false;
|
||||
if (treeNode.rootElements.some(rootElement => !scope.contains(rootElement)))
|
||||
if (treeNode.rootElements.some(rootElement => !isInsideScope(scope, rootElement)))
|
||||
return false;
|
||||
for (const attr of attributes) {
|
||||
if (!checkComponentAttribute(treeNode.props, attr))
|
||||
|
|
|
|||
|
|
@ -134,6 +134,18 @@ for (const [name, url] of Object.entries(reacts)) {
|
|||
await expect(page.locator('css=#root2 >> _react=BookItem')).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiroot react inside shadow DOM', async ({ page }) => {
|
||||
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
||||
await page.evaluate(() => {
|
||||
const anotherRoot = document.createElement('div');
|
||||
document.body.append(anotherRoot);
|
||||
const shadowRoot = anotherRoot.attachShadow({ mode: 'open' });
|
||||
// @ts-ignore
|
||||
window.mountApp(shadowRoot);
|
||||
});
|
||||
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,6 +132,26 @@ for (const [name, url] of Object.entries(vues)) {
|
|||
await expect(page.locator('css=#root2 >> _vue=book-item')).toHaveCount(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with multiroot vue inside shadow DOM', async ({ page }) => {
|
||||
await expect(page.locator(`_vue=book-item`)).toHaveCount(3);
|
||||
await page.evaluate(vueName => {
|
||||
const anotherRoot = document.createElement('div');
|
||||
document.body.append(anotherRoot);
|
||||
const shadowRoot = anotherRoot.attachShadow({ mode: 'open' });
|
||||
if (vueName === 'vue2') {
|
||||
// Vue2 cannot be mounted in shadow root directly.
|
||||
const div = document.createElement('div');
|
||||
shadowRoot.append(div);
|
||||
// @ts-ignore
|
||||
window.mountApp(div);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.mountApp(shadowRoot);
|
||||
}
|
||||
}, name);
|
||||
await expect(page.locator(`_vue=book-item`)).toHaveCount(6);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue