fix(react-vue): support nested trees (#8467)
Turns out you can mount nested trees in both React and Vue. This patch changes root discovery to support nested trees. Fixes #8455
This commit is contained in:
parent
222151f2e1
commit
2198769f6c
|
|
@ -134,24 +134,22 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
||||||
|
|
||||||
function findReactRoots(): ReactVNode[] {
|
function findReactRoots(): ReactVNode[] {
|
||||||
const roots: ReactVNode[] = [];
|
const roots: ReactVNode[] = [];
|
||||||
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, {
|
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);
|
||||||
acceptNode: function(node) {
|
while (walker.nextNode()) {
|
||||||
|
const node = walker.currentNode;
|
||||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L329
|
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L329
|
||||||
if (node.hasOwnProperty('_reactRootContainer')) {
|
if (node.hasOwnProperty('_reactRootContainer'))
|
||||||
roots.push((node as any)._reactRootContainer._internalRoot.current);
|
roots.push((node as any)._reactRootContainer._internalRoot.current);
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
}
|
||||||
|
// 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)) {
|
for (const key of Object.keys(node)) {
|
||||||
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L334
|
// @see https://github.com/baruchvlz/resq/blob/5c15a5e04d3f7174087248f5a158c3d6dcc1ec72/src/utils.js#L334
|
||||||
if (key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber')) {
|
if (key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber'))
|
||||||
roots.push((node as any)[key]);
|
roots.push((node as any)[key]);
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
while (walker.nextNode());
|
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,22 +207,25 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
||||||
type VueRoot = {version: number, root: VueVNode};
|
type VueRoot = {version: number, root: VueVNode};
|
||||||
function findVueRoots(): VueRoot[] {
|
function findVueRoots(): VueRoot[] {
|
||||||
const roots: VueRoot[] = [];
|
const roots: VueRoot[] = [];
|
||||||
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, {
|
// Vue3 roots are marked with [data-v-app] attribute
|
||||||
acceptNode: function(node) {
|
for (const node of document.querySelectorAll('[data-v-app]')) {
|
||||||
// Vue3 root
|
if ((node as any)._vnode && (node as any)._vnode.component)
|
||||||
if ((node as any)._vnode && (node as any)._vnode.component) {
|
|
||||||
roots.push({root: (node as any)._vnode.component, version: 3});
|
roots.push({root: (node as any)._vnode.component, version: 3});
|
||||||
return NodeFilter.FILTER_REJECT;
|
|
||||||
}
|
}
|
||||||
// Vue2 root
|
// Vue2 roots are referred to from elements.
|
||||||
if ((node as any).__vue__) {
|
const walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT);
|
||||||
roots.push({root: (node as any).__vue__, version: 2});
|
const vue2Roots: Set<VueVNode> = new Set();
|
||||||
return NodeFilter.FILTER_REJECT;
|
while (walker.nextNode()) {
|
||||||
}
|
const element = walker.currentNode as any;
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
if (element && element.__vue__)
|
||||||
|
vue2Roots.add(element.__vue__.$root);
|
||||||
}
|
}
|
||||||
|
for (const vue2root of vue2Roots) {
|
||||||
|
roots.push({
|
||||||
|
version: 2,
|
||||||
|
root: vue2root,
|
||||||
});
|
});
|
||||||
while (walker.nextNode());
|
}
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ class App extends React.Component {
|
||||||
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
||||||
e(BookList, {books: this.state.books}, null),
|
e(BookList, {books: this.state.books}, null),
|
||||||
e(ButtonGrid, null, null),
|
e(ButtonGrid, null, null),
|
||||||
|
e('div', { ref: 'mountPoint', }, null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +103,7 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
||||||
window.mountApp(document.getElementById('root'));
|
window.app = window.mountApp(document.getElementById('root'));
|
||||||
|
window.mountNestedApp = () => window.mountApp(window.app.refs.mountPoint);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ class BookList extends React.Component {
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.mountPoint = React.createRef();
|
||||||
this.state = {
|
this.state = {
|
||||||
books: [
|
books: [
|
||||||
{name: 'Pride and Prejudice' },
|
{name: 'Pride and Prejudice' },
|
||||||
|
|
@ -89,6 +90,7 @@ class App extends React.Component {
|
||||||
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
||||||
e(BookList, {books: this.state.books}, null),
|
e(BookList, {books: this.state.books}, null),
|
||||||
e(ButtonGrid, null, null),
|
e(ButtonGrid, null, null),
|
||||||
|
e('div', {ref: this.mountPoint}, null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +102,8 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
||||||
window.mountApp(document.getElementById('root'));
|
window.app = window.mountApp(document.getElementById('root'));
|
||||||
|
|
||||||
|
window.mountNestedApp = () => window.mountApp(window.app.mountPoint.current);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ class BookList extends React.Component {
|
||||||
class App extends React.Component {
|
class App extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.mountPoint = React.createRef();
|
||||||
this.state = {
|
this.state = {
|
||||||
books: [
|
books: [
|
||||||
{name: 'Pride and Prejudice' },
|
{name: 'Pride and Prejudice' },
|
||||||
|
|
@ -83,6 +84,7 @@ class App extends React.Component {
|
||||||
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
e(NewBook, {onNewBook: bookName => this.onNewBook(bookName)}, null),
|
||||||
e(BookList, {books: this.state.books}, null),
|
e(BookList, {books: this.state.books}, null),
|
||||||
e(ButtonGrid, null, null),
|
e(ButtonGrid, null, null),
|
||||||
|
e('div', {ref: this.mountPoint}, null),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
window.mountApp = element => ReactDOM.render(e(App, null, null), element);
|
||||||
window.mountApp(document.getElementById('root'));
|
window.app = window.mountApp(document.getElementById('root'));
|
||||||
|
window.mountNestedApp = () => window.mountApp(window.app.mountPoint.current);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ window.mountApp = element => new Vue({
|
||||||
<new-book @newbook='addNewBook'></new-book>
|
<new-book @newbook='addNewBook'></new-book>
|
||||||
<book-list :books='books'></book-list>
|
<book-list :books='books'></book-list>
|
||||||
<button-grid></button-grid>
|
<button-grid></button-grid>
|
||||||
|
<div ref="mountPoint"></div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
|
@ -111,6 +112,7 @@ window.mountApp = element => new Vue({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.mountApp(document.querySelector('#root div'));
|
window.app = window.mountApp(document.querySelector('#root div'));
|
||||||
|
window.mountNestedApp = () => window.mountApp(window.app.$refs.mountPoint);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ window.mountApp = element => {
|
||||||
<new-book @newbook='addNewBook'></new-book>
|
<new-book @newbook='addNewBook'></new-book>
|
||||||
<book-list :books='books'></book-list>
|
<book-list :books='books'></book-list>
|
||||||
<button-grid></button-grid>
|
<button-grid></button-grid>
|
||||||
|
<div ref="mountPoint"></div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -26,7 +27,6 @@ window.mountApp = element => {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
addNewBook(name) {
|
addNewBook(name) {
|
||||||
console.log('here');
|
|
||||||
this.books.push({name});
|
this.books.push({name});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -107,9 +107,10 @@ window.mountApp = element => {
|
||||||
return buttons;
|
return buttons;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.mount(element);
|
return app.mount(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.mountApp(document.querySelector('#root div'));
|
window.app = window.mountApp(document.querySelector('#root div'));
|
||||||
|
window.mountNestedApp = () => window.mountApp(window.app.$refs.mountPoint);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
|
|
||||||
it('should work with multi-root elements (fragments)', async ({page}) => {
|
it('should work with multi-root elements (fragments)', async ({page}) => {
|
||||||
it.skip(name === 'react15', 'React 15 does not support fragments');
|
it.skip(name === 'react15', 'React 15 does not support fragments');
|
||||||
expect(await page.$$eval(`_react=App`, els => els.length)).toBe(14);
|
expect(await page.$$eval(`_react=App`, els => els.length)).toBe(15);
|
||||||
expect(await page.$$eval(`_react=AppHeader`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`_react=AppHeader`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`_react=NewBook`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`_react=NewBook`, els => els.length)).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
@ -105,6 +105,15 @@ for (const [name, url] of Object.entries(reacts)) {
|
||||||
expect(await page.$$eval(`_react=ColorButton[enabled]`, els => els.length)).toBe(5);
|
expect(await page.$$eval(`_react=ColorButton[enabled]`, els => els.length)).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support nested react trees', async ({page}) => {
|
||||||
|
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mountNestedApp();
|
||||||
|
});
|
||||||
|
await expect(page.locator(`_react=BookItem`)).toHaveCount(6);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with multiroot react', async ({page}) => {
|
it('should work with multiroot react', async ({page}) => {
|
||||||
await it.step('mount second root', async () => {
|
await it.step('mount second root', async () => {
|
||||||
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
await expect(page.locator(`_react=BookItem`)).toHaveCount(3);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ for (const [name, url] of Object.entries(vues)) {
|
||||||
|
|
||||||
it('should work with multi-root elements (fragments)', async ({page}) => {
|
it('should work with multi-root elements (fragments)', async ({page}) => {
|
||||||
it.skip(name === 'vue2', 'vue2 does not support fragments');
|
it.skip(name === 'vue2', 'vue2 does not support fragments');
|
||||||
expect(await page.$$eval(`_vue=Root`, els => els.length)).toBe(14);
|
expect(await page.$$eval(`_vue=Root`, els => els.length)).toBe(15);
|
||||||
expect(await page.$$eval(`_vue=app-header`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`_vue=app-header`, els => els.length)).toBe(2);
|
||||||
expect(await page.$$eval(`_vue=new-book`, els => els.length)).toBe(2);
|
expect(await page.$$eval(`_vue=new-book`, els => els.length)).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
@ -102,6 +102,15 @@ for (const [name, url] of Object.entries(vues)) {
|
||||||
expect(await page.$$eval(`_vue=color-button[enabled]`, els => els.length)).toBe(5);
|
expect(await page.$$eval(`_vue=color-button[enabled]`, els => els.length)).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should support nested vue trees', async ({page}) => {
|
||||||
|
await expect(page.locator(`_vue=book-item`)).toHaveCount(3);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
mountNestedApp();
|
||||||
|
});
|
||||||
|
await expect(page.locator(`_vue=book-item`)).toHaveCount(6);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with multiroot react', async ({page}) => {
|
it('should work with multiroot react', async ({page}) => {
|
||||||
await it.step('mount second root', async () => {
|
await it.step('mount second root', async () => {
|
||||||
await expect(page.locator(`_vue=book-item`)).toHaveCount(3);
|
await expect(page.locator(`_vue=book-item`)).toHaveCount(3);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue