test(ssr): hydration suspense tests
This commit is contained in:
		
							parent
							
								
									1f9c9c14ae
								
							
						
					
					
						commit
						eb1d538ea2
					
				| @ -5,7 +5,9 @@ import { | |||||||
|   nextTick, |   nextTick, | ||||||
|   VNode, |   VNode, | ||||||
|   Portal, |   Portal, | ||||||
|   createStaticVNode |   createStaticVNode, | ||||||
|  |   Suspense, | ||||||
|  |   onMounted | ||||||
| } from '@vue/runtime-dom' | } from '@vue/runtime-dom' | ||||||
| import { renderToString } from '@vue/server-renderer' | import { renderToString } from '@vue/server-renderer' | ||||||
| import { mockWarn } from '@vue/shared' | import { mockWarn } from '@vue/shared' | ||||||
| @ -28,6 +30,8 @@ const triggerEvent = (type: string, el: Element) => { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| describe('SSR hydration', () => { | describe('SSR hydration', () => { | ||||||
|  |   mockWarn() | ||||||
|  | 
 | ||||||
|   test('text', async () => { |   test('text', async () => { | ||||||
|     const msg = ref('foo') |     const msg = ref('foo') | ||||||
|     const { vnode, container } = mountWithHydration('foo', () => msg.value) |     const { vnode, container } = mountWithHydration('foo', () => msg.value) | ||||||
| @ -94,7 +98,7 @@ describe('SSR hydration', () => { | |||||||
|     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`) |     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   test('fragment', async () => { |   test('Fragment', async () => { | ||||||
|     const msg = ref('foo') |     const msg = ref('foo') | ||||||
|     const fn = jest.fn() |     const fn = jest.fn() | ||||||
|     const { vnode, container } = mountWithHydration( |     const { vnode, container } = mountWithHydration( | ||||||
| @ -142,7 +146,7 @@ describe('SSR hydration', () => { | |||||||
|     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`) |     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   test('portal', async () => { |   test('Portal', async () => { | ||||||
|     const msg = ref('foo') |     const msg = ref('foo') | ||||||
|     const fn = jest.fn() |     const fn = jest.fn() | ||||||
|     const portalContainer = document.createElement('div') |     const portalContainer = document.createElement('div') | ||||||
| @ -271,9 +275,109 @@ describe('SSR hydration', () => { | |||||||
|     expect(text.textContent).toBe('bye') |     expect(text.textContent).toBe('bye') | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   describe('mismatch handling', () => { |   test('Suspense', async () => { | ||||||
|     mockWarn() |     const AsyncChild = { | ||||||
|  |       async setup() { | ||||||
|  |         const count = ref(0) | ||||||
|  |         return () => | ||||||
|  |           h( | ||||||
|  |             'span', | ||||||
|  |             { | ||||||
|  |               onClick: () => { | ||||||
|  |                 count.value++ | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             count.value | ||||||
|  |           ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const { vnode, container } = mountWithHydration('<span>0</span>', () => | ||||||
|  |       h(Suspense, () => h(AsyncChild)) | ||||||
|  |     ) | ||||||
|  |     expect(vnode.el).toBe(container.firstChild) | ||||||
|  |     // wait for hydration to finish
 | ||||||
|  |     await new Promise(r => setTimeout(r)) | ||||||
|  |     triggerEvent('click', container.querySelector('span')!) | ||||||
|  |     await nextTick() | ||||||
|  |     expect(container.innerHTML).toBe(`<span>1</span>`) | ||||||
|  |   }) | ||||||
| 
 | 
 | ||||||
|  |   test('Suspense (full integration)', async () => { | ||||||
|  |     const mountedCalls: number[] = [] | ||||||
|  |     const asyncDeps: Promise<any>[] = [] | ||||||
|  | 
 | ||||||
|  |     const AsyncChild = { | ||||||
|  |       async setup(props: { n: number }) { | ||||||
|  |         const count = ref(props.n) | ||||||
|  |         onMounted(() => { | ||||||
|  |           mountedCalls.push(props.n) | ||||||
|  |         }) | ||||||
|  |         const p = new Promise(r => setTimeout(r, props.n * 10)) | ||||||
|  |         asyncDeps.push(p) | ||||||
|  |         await p | ||||||
|  |         return () => | ||||||
|  |           h( | ||||||
|  |             'span', | ||||||
|  |             { | ||||||
|  |               onClick: () => { | ||||||
|  |                 count.value++ | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             count.value | ||||||
|  |           ) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const done = jest.fn() | ||||||
|  |     const App = { | ||||||
|  |       template: ` | ||||||
|  |       <Suspense @resolve="done"> | ||||||
|  |         <AsyncChild :n="1" /> | ||||||
|  |         <AsyncChild :n="2" /> | ||||||
|  |       </Suspense>`,
 | ||||||
|  |       components: { | ||||||
|  |         AsyncChild | ||||||
|  |       }, | ||||||
|  |       methods: { | ||||||
|  |         done | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const container = document.createElement('div') | ||||||
|  |     // server render
 | ||||||
|  |     container.innerHTML = await renderToString(h(App)) | ||||||
|  |     expect(container.innerHTML).toMatchInlineSnapshot( | ||||||
|  |       `"<!--[--><span>1</span><span>2</span><!--]-->"` | ||||||
|  |     ) | ||||||
|  |     // reset asyncDeps from ssr
 | ||||||
|  |     asyncDeps.length = 0 | ||||||
|  |     // hydrate
 | ||||||
|  |     createSSRApp(App).mount(container) | ||||||
|  | 
 | ||||||
|  |     expect(mountedCalls.length).toBe(0) | ||||||
|  |     expect(asyncDeps.length).toBe(2) | ||||||
|  | 
 | ||||||
|  |     // wait for hydration to complete
 | ||||||
|  |     await Promise.all(asyncDeps) | ||||||
|  |     await new Promise(r => setTimeout(r)) | ||||||
|  | 
 | ||||||
|  |     // should flush buffered effects
 | ||||||
|  |     expect(mountedCalls).toMatchObject([1, 2]) | ||||||
|  |     // should have removed fragment markers
 | ||||||
|  |     expect(container.innerHTML).toMatch(`<span>1</span><span>2</span>`) | ||||||
|  | 
 | ||||||
|  |     const span1 = container.querySelector('span')! | ||||||
|  |     triggerEvent('click', span1) | ||||||
|  |     await nextTick() | ||||||
|  |     expect(container.innerHTML).toMatch(`<span>2</span><span>2</span>`) | ||||||
|  | 
 | ||||||
|  |     const span2 = span1.nextSibling as Element | ||||||
|  |     triggerEvent('click', span2) | ||||||
|  |     await nextTick() | ||||||
|  |     expect(container.innerHTML).toMatch(`<span>2</span><span>3</span>`) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   describe('mismatch handling', () => { | ||||||
|     test('text node', () => { |     test('text node', () => { | ||||||
|       const { container } = mountWithHydration(`foo`, () => 'bar') |       const { container } = mountWithHydration(`foo`, () => 'bar') | ||||||
|       expect(container.textContent).toBe('bar') |       expect(container.textContent).toBe('bar') | ||||||
|  | |||||||
| @ -47,7 +47,6 @@ export function createHydrationFunctions( | |||||||
|   const { |   const { | ||||||
|     mt: mountComponent, |     mt: mountComponent, | ||||||
|     p: patch, |     p: patch, | ||||||
|     n: next, |  | ||||||
|     o: { patchProp, nextSibling, parentNode } |     o: { patchProp, nextSibling, parentNode } | ||||||
|   } = rendererInternals |   } = rendererInternals | ||||||
| 
 | 
 | ||||||
| @ -152,16 +151,12 @@ export function createHydrationFunctions( | |||||||
|             parentSuspense, |             parentSuspense, | ||||||
|             isSVGContainer(container) |             isSVGContainer(container) | ||||||
|           ) |           ) | ||||||
|           const subTree = vnode.component!.subTree |           // component may be async, so in the case of fragments we cannot rely
 | ||||||
|           if (subTree) { |           // on component's rendered output to determine the end of the fragment
 | ||||||
|             return next(subTree) |           // instead, we do a lookahead to find the end anchor node.
 | ||||||
|           } else { |           return isFragmentStart | ||||||
|             // no subTree means this is an async component
 |             ? locateClosingAsyncAnchor(node) | ||||||
|             // try to locate the ending node
 |             : nextSibling(node) | ||||||
|             return isFragmentStart |  | ||||||
|               ? locateClosingAsyncAnchor(node) |  | ||||||
|               : nextSibling(node) |  | ||||||
|           } |  | ||||||
|         } else if (shapeFlag & ShapeFlags.PORTAL) { |         } else if (shapeFlag & ShapeFlags.PORTAL) { | ||||||
|           if (domType !== DOMNodeTypes.COMMENT) { |           if (domType !== DOMNodeTypes.COMMENT) { | ||||||
|             return handleMismtach(node, vnode, parentComponent, parentSuspense) |             return handleMismtach(node, vnode, parentComponent, parentSuspense) | ||||||
|  | |||||||
| @ -212,7 +212,6 @@ export function createVNode( | |||||||
| ): VNode { | ): VNode { | ||||||
|   if (!type) { |   if (!type) { | ||||||
|     if (__DEV__) { |     if (__DEV__) { | ||||||
|       debugger |  | ||||||
|       warn(`fsef Invalid vnode type when creating vnode: ${type}.`) |       warn(`fsef Invalid vnode type when creating vnode: ${type}.`) | ||||||
|     } |     } | ||||||
|     type = Comment |     type = Comment | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user