Repository: vuejs/test-utils
Pull request #2579 (2024-12-31 14:31:56)

fix: text() misses content for array functional component
Fix for https://github.com/vuejs/test-utils/issues/2568

This change adds a new optional parameter to the DOMWrapper constructor so information about its structure can be later used to produce correct `text()` and `html()` assertions.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *vue-test-utils-docs* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | b03d20826d28769971407197df685729db2cbbaf |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/vue-test-utils-docs/deploys/6774005ec9d70a000869259b |
miguelrincon: @cexbrayat This is an initial idea to fix https://github.com/vuejs/test-utils/issues/2568, all tests are passing but I hacked a bit around the types and method signatures. I'll need help to get those fixed ;)

I am also not sure if changing the `DOMWrapper` constructor to take a `vnode` it's a better fit, as the `vnode` contains more information we may need (rather than just the new subtree param), seems like a bigger change than I'd like to take on.

cc @xanf 
miguelrincon: @cexbrayat Thanks for the response. I removed the comment and rebased. Can you have another look?
cexbrayat: @miguelrincon It looks like the linter in unhappy, can you take a look? thank you!

Patch:
diff --git a/src/baseWrapper.ts b/src/baseWrapper.ts
index 63bc597..9f2f904 100644
--- a/src/baseWrapper.ts
+++ b/src/baseWrapper.ts
@@ -1,3 +1,4 @@
+import { VNode } from 'vue'
 import { textContent } from './utils'
 import type { TriggerOptions } from './createDomEvent'
 import {
@@ -219,7 +220,7 @@ export default abstract class BaseWrapper<ElementType extends Node>
     return results.map((c) =>
       c.proxy
         ? createVueWrapper(null, c.proxy)
-        : createDOMWrapper(c.vnode.el as Element)
+        : createDOMWrapper(c.vnode.el as Element, c.subTree as VNode)
     )
   }
   abstract setValue(value?: any): Promise<void>
diff --git a/src/domWrapper.ts b/src/domWrapper.ts
index 2d9b62a..92b38ab 100644
--- a/src/domWrapper.ts
+++ b/src/domWrapper.ts
@@ -1,3 +1,4 @@
+import { VNode } from 'vue'
 import { config } from './config'
 import BaseWrapper from './baseWrapper'
 import WrapperLike from './interfaces/wrapperLike'
@@ -11,16 +12,23 @@ import { isRefSelector } from './utils'
 import { createWrapperError } from './errorWrapper'
 
 export class DOMWrapper<NodeType extends Node> extends BaseWrapper<NodeType> {
-  constructor(element: NodeType | null | undefined) {
+  protected readonly subTree: VNode | null | undefined = null
+
+  constructor(element: NodeType | null | undefined, subTree?: VNode | null) {
     if (!element) {
       return createWrapperError('DOMWrapper')
     }
     super(element)
+    this.subTree = subTree
+
     // plugins hook
     config.plugins.DOMWrapper.extend(this)
   }
 
   getRootNodes() {
+    if (Array.isArray(this.subTree?.children)) {
+      return this.subTree.children.map((node) => (node as any)?.el)
+    }
     return [this.wrapperElement]
   }
 
@@ -64,7 +72,7 @@ export class DOMWrapper<NodeType extends Node> extends BaseWrapper<NodeType> {
     }
     return Array.from(
       this.wrapperElement.querySelectorAll(selector),
-      createDOMWrapper
+      (element) => createDOMWrapper(element)
     )
   }
 
@@ -155,4 +163,7 @@ export class DOMWrapper<NodeType extends Node> extends BaseWrapper<NodeType> {
   }
 }
 
-registerFactory(WrapperType.DOMWrapper, (element) => new DOMWrapper(element))
+registerFactory(
+  WrapperType.DOMWrapper,
+  (element, subTree) => new DOMWrapper(element, subTree)
+)
diff --git a/src/vueWrapper.ts b/src/vueWrapper.ts
index d4eb173..eae562f 100644
--- a/src/vueWrapper.ts
+++ b/src/vueWrapper.ts
@@ -173,7 +173,7 @@ export class VueWrapper<
   ): DOMWrapper<SVGElementTagNameMap[K]>[]
   findAll<T extends Element>(selector: string): DOMWrapper<T>[]
   findAll(selector: string): DOMWrapper<Element>[] {
-    return this.findAllDOMElements(selector).map(createDOMWrapper)
+    return this.findAllDOMElements(selector).map((e) => createDOMWrapper(e))
   }
 
   private attachNativeEventListener(): void {
diff --git a/src/wrapperFactory.ts b/src/wrapperFactory.ts
index 429773e..b187e27 100644
--- a/src/wrapperFactory.ts
+++ b/src/wrapperFactory.ts
@@ -1,4 +1,4 @@
-import { ComponentPublicInstance, App } from 'vue'
+import { ComponentPublicInstance, App, VNode } from 'vue'
 import type { DOMWrapper as DOMWrapperType } from './domWrapper'
 import type { VueWrapper as VueWrapperType } from './vueWrapper'
 
@@ -8,7 +8,8 @@ export enum WrapperType {
 }
 
 type DOMWrapperFactory = <T extends Node>(
-  element: T | null | undefined
+  element: T | null | undefined,
+  subTree?: VNode
 ) => DOMWrapperType<T>
 type VueWrapperFactory = <T extends ComponentPublicInstance>(
   app: App | null,
@@ -36,7 +37,7 @@ export function registerFactory(
   factories[type] = fn
 }
 
-export const createDOMWrapper: DOMWrapperFactory = (element) =>
-  factories[WrapperType.DOMWrapper]!(element)
+export const createDOMWrapper: DOMWrapperFactory = (element, subTree) =>
+  factories[WrapperType.DOMWrapper]!(element, subTree)
 export const createVueWrapper: VueWrapperFactory = (app, vm, setProps) =>
   factories[WrapperType.VueWrapper]!(app, vm, setProps)


Test patch:
diff --git a/tests/element.spec.ts b/tests/element.spec.ts
index 9d4d39d..c3abcd2 100644
--- a/tests/element.spec.ts
+++ b/tests/element.spec.ts
@@ -60,6 +60,36 @@ describe('element', () => {
     expect(wrapper.text()).toBe('foobarbaz')
   })
 
+  it('returns correct output for functional component with multiple text roots', () => {
+    const Func = () => ['foo', 'bar']
+
+    const Parent = defineComponent({
+      name: 'Parent',
+      components: { Func },
+      template: '<div><Func/></div>'
+    })
+    const wrapper = mount(Parent)
+
+    expect(wrapper.findComponent(Func).html()).toBe('foo\nbar')
+    expect(wrapper.findComponent(Func).text()).toBe('foobar')
+  })
+
+  it('returns correct output for functional component with multiple element roots', () => {
+    const Func = () => [h('div', {}, 'foo'), h('div', {}, 'bar')]
+
+    const Parent = defineComponent({
+      name: 'Parent',
+      components: { Func },
+      template: '<div><Func/></div>'
+    })
+    const wrapper = mount(Parent)
+
+    expect(wrapper.findComponent(Func).html()).toBe(
+      '<div>foo</div>\n<div>bar</div>'
+    )
+    expect(wrapper.findComponent(Func).text()).toBe('foobar')
+  })
+
   it('returns correct element for root slot', () => {
     const Parent = defineComponent({
       components: { ReturnSlot },