refactor: fix implementation of SFC :slotted id handling

fix #2892
This commit is contained in:
Evan You
2021-03-05 11:10:06 -05:00
parent cc975c1292
commit aea88c3280
36 changed files with 723 additions and 457 deletions

View File

@@ -14,8 +14,7 @@ import {
ComponentPublicInstance,
Ref,
cloneVNode,
provide,
withScopeId
provide
} from '@vue/runtime-test'
import { KeepAliveProps } from '../../src/components/KeepAlive'
@@ -804,14 +803,13 @@ describe('KeepAlive', () => {
test('should work with cloned root due to scopeId / fallthrough attrs', async () => {
const viewRef = ref('one')
const instanceRef = ref<any>(null)
const withId = withScopeId('foo')
const App = {
__scopeId: 'foo',
render: withId(() => {
render: () => {
return h(KeepAlive, null, {
default: () => h(views[viewRef.value], { ref: instanceRef })
})
})
}
}
render(h(App), root)
expect(serializeInner(root)).toBe(`<div foo>one</div>`)

View File

@@ -8,7 +8,7 @@ import {
Fragment,
createCommentVNode
} from '../../src'
import { PatchFlags } from '@vue/shared/src'
import { PatchFlags } from '@vue/shared'
describe('renderSlot', () => {
it('should render slot', () => {
@@ -37,7 +37,7 @@ describe('renderSlot', () => {
return [createVNode('div', null, 'foo', PatchFlags.TEXT)]
},
// mock instance
{} as any
{ type: {} } as any
)
// manual invocation should not track

View File

@@ -1,105 +0,0 @@
import { withScopeId } from '../../src/helpers/scopeId'
import { h, render, nodeOps, serializeInner } from '@vue/runtime-test'
describe('scopeId runtime support', () => {
const withParentId = withScopeId('parent')
const withChildId = withScopeId('child')
test('should attach scopeId', () => {
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h('div', [h('div')])
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
})
test('should attach scopeId to components in parent component', () => {
const Child = {
__scopeId: 'child',
render: withChildId(() => {
return h('div')
})
}
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h('div', [h(Child)])
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(
`<div parent><div child parent></div></div>`
)
})
test('should work on slots', () => {
const Child = {
__scopeId: 'child',
render: withChildId(function(this: any) {
return h('div', this.$slots.default())
})
}
const withChild2Id = withScopeId('child2')
const Child2 = {
__scopeId: 'child2',
render: withChild2Id(() => h('span'))
}
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h(
Child,
withParentId(() => {
return [h('div'), h(Child2)]
})
)
})
}
const root = nodeOps.createElement('div')
render(h(App), root)
// slot content should have:
// - scopeId from parent
// - slotted scopeId (with `-s` postfix) from child (the tree owner)
expect(serializeInner(root)).toBe(
`<div child parent>` +
`<div parent child-s></div>` +
// component inside slot should have:
// - scopeId from template context
// - slotted scopeId from slot owner
// - its own scopeId
`<span child2 parent child-s></span>` +
`</div>`
)
})
// #1988
test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => {
const withParentId = withScopeId('parent')
const App = {
__scopeId: 'parent',
render: withParentId(() => {
return h(Child)
})
}
function Child() {
return h(Child2, { class: 'foo' })
}
function Child2() {
return h('div')
}
Child2.inheritAttrs = false
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div parent></div>`)
})
})

View File

@@ -0,0 +1,178 @@
import {
h,
render,
nodeOps,
serializeInner,
renderSlot
} from '@vue/runtime-test'
import { setScopeId, withCtx } from '../src/componentRenderContext'
describe('scopeId runtime support', () => {
test('should attach scopeId', () => {
const App = {
__scopeId: 'parent',
render: () => h('div', [h('div')])
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
})
test('should attach scopeId to components in parent component', () => {
const Child = {
__scopeId: 'child',
render: () => h('div')
}
const App = {
__scopeId: 'parent',
render: () => h('div', [h(Child)])
}
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(
`<div parent><div child parent></div></div>`
)
})
// :slotted basic
test('should work on slots', () => {
const Child = {
__scopeId: 'child',
render(this: any) {
return h('div', renderSlot(this.$slots, 'default'))
}
}
const Child2 = {
__scopeId: 'child2',
render: () => h('span')
}
const App = {
__scopeId: 'parent',
render: () => {
return h(
Child,
withCtx(() => {
return [h('div'), h(Child2)]
})
)
}
}
const root = nodeOps.createElement('div')
render(h(App), root)
// slot content should have:
// - scopeId from parent
// - slotted scopeId (with `-s` postfix) from child (the tree owner)
expect(serializeInner(root)).toBe(
`<div child parent>` +
`<div parent child-s></div>` +
// component inside slot should have:
// - scopeId from template context
// - slotted scopeId from slot owner
// - its own scopeId
`<span child2 parent child-s></span>` +
`</div>`
)
})
// #2892
test(':slotted on forwarded slots', async () => {
const Wrapper = {
__scopeId: 'wrapper',
render(this: any) {
// <div class="wrapper"><slot/></div>
return h('div', { class: 'wrapper' }, [
renderSlot(this.$slots, 'default')
])
}
}
const Slotted = {
__scopeId: 'slotted',
render(this: any) {
// <Wrapper><slot/></Wrapper>
return h(Wrapper, null, {
default: withCtx(() => [renderSlot(this.$slots, 'default')])
})
}
}
// simulate hoisted node
setScopeId('root')
const hoisted = h('div', 'hoisted')
setScopeId(null)
const Root = {
__scopeId: 'root',
render(this: any) {
// <Slotted><div>hoisted</div><div>{{ dynamic }}</div></Slotted>
return h(Slotted, null, {
default: withCtx(() => {
return [hoisted, h('div', 'dynamic')]
})
})
}
}
const root = nodeOps.createElement('div')
render(h(Root), root)
expect(serializeInner(root)).toBe(
`<div class="wrapper" wrapper slotted root>` +
`<div root wrapper-s slotted-s>hoisted</div>` +
`<div root wrapper-s slotted-s>dynamic</div>` +
`</div>`
)
const Root2 = {
__scopeId: 'root',
render(this: any) {
// <Slotted>
// <Wrapper>
// <div>hoisted</div><div>{{ dynamic }}</div>
// </Wrapper>
// </Slotted>
return h(Slotted, null, {
default: withCtx(() => [
h(Wrapper, null, {
default: withCtx(() => [hoisted, h('div', 'dynamic')])
})
])
})
}
}
const root2 = nodeOps.createElement('div')
render(h(Root2), root2)
expect(serializeInner(root2)).toBe(
`<div class="wrapper" wrapper slotted root>` +
`<div class="wrapper" wrapper root wrapper-s slotted-s>` +
`<div root wrapper-s>hoisted</div>` +
`<div root wrapper-s>dynamic</div>` +
`</div>` +
`</div>`
)
})
// #1988
test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => {
const App = {
__scopeId: 'parent',
render: () => {
return h(Child)
}
}
function Child() {
return h(Child2, { class: 'foo' })
}
function Child2() {
return h('div')
}
Child2.inheritAttrs = false
const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div parent></div>`)
})
})

View File

@@ -14,7 +14,7 @@ import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h, reactive, isReactive, setBlockTracking } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
import { setCurrentRenderingInstance } from '../src/componentRenderUtils'
import { setCurrentRenderingInstance } from '../src/componentRenderContext'
describe('vnode', () => {
test('create with just tag', () => {
@@ -231,8 +231,8 @@ describe('vnode', () => {
// ref normalizes to [currentRenderingInstance, ref]
test('cloneVNode ref normalization', () => {
const mockInstance1 = {} as any
const mockInstance2 = {} as any
const mockInstance1 = { type: {} } as any
const mockInstance2 = { type: {} } as any
setCurrentRenderingInstance(mockInstance1)
const original = createVNode('div', { ref: 'foo' })
@@ -272,8 +272,8 @@ describe('vnode', () => {
})
test('cloneVNode ref merging', () => {
const mockInstance1 = {} as any
const mockInstance2 = {} as any
const mockInstance1 = { type: {} } as any
const mockInstance2 = { type: {} } as any
setCurrentRenderingInstance(mockInstance1)
const original = createVNode('div', { ref: 'foo' })