scopeId.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import {
  2. Fragment,
  3. Suspense,
  4. createBlock,
  5. createCommentVNode,
  6. createVNode,
  7. defineComponent,
  8. h,
  9. nextTick,
  10. nodeOps,
  11. openBlock,
  12. popScopeId,
  13. pushScopeId,
  14. ref,
  15. render,
  16. renderSlot,
  17. serializeInner,
  18. withScopeId,
  19. } from '@vue/runtime-test'
  20. import { withCtx } from '../src/componentRenderContext'
  21. import { PatchFlags } from '@vue/shared'
  22. describe('scopeId runtime support', () => {
  23. test('should attach scopeId', () => {
  24. const App = {
  25. __scopeId: 'parent',
  26. render: () => h('div', [h('div')]),
  27. }
  28. const root = nodeOps.createElement('div')
  29. render(h(App), root)
  30. expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
  31. })
  32. test('should attach scopeId to components in parent component', () => {
  33. const Child = {
  34. __scopeId: 'child',
  35. render: () => h('div'),
  36. }
  37. const App = {
  38. __scopeId: 'parent',
  39. render: () => h('div', [h(Child)]),
  40. }
  41. const root = nodeOps.createElement('div')
  42. render(h(App), root)
  43. expect(serializeInner(root)).toBe(
  44. `<div parent><div child parent></div></div>`,
  45. )
  46. })
  47. // #5148
  48. test('should attach scopeId to suspense content', async () => {
  49. const deps: Promise<any>[] = []
  50. const Child = {
  51. async setup() {
  52. const p = new Promise(r => setTimeout(r, 1))
  53. deps.push(p.then(() => Promise.resolve()))
  54. await p
  55. return () => h('div', 'async')
  56. },
  57. }
  58. const Wrapper = {
  59. setup(_: any, { slots }: any) {
  60. return () => slots.default({ Component: h(Child) })
  61. },
  62. }
  63. const App = {
  64. __scopeId: 'parent',
  65. setup() {
  66. return () =>
  67. h(Wrapper, null, {
  68. default: withCtx(({ Component }: any) =>
  69. h(Suspense, null, {
  70. default: h(Component),
  71. fallback: h('div', 'fallback'),
  72. }),
  73. ),
  74. })
  75. },
  76. }
  77. const root = nodeOps.createElement('div')
  78. render(h(App), root)
  79. expect(serializeInner(root)).toBe(`<div parent>fallback</div>`)
  80. await Promise.all(deps)
  81. await nextTick()
  82. expect(serializeInner(root)).toBe(`<div parent>async</div>`)
  83. })
  84. // :slotted basic
  85. test('should work on slots', () => {
  86. const Child = {
  87. __scopeId: 'child',
  88. render(this: any) {
  89. return h('div', renderSlot(this.$slots, 'default'))
  90. },
  91. }
  92. const Child2 = {
  93. __scopeId: 'child2',
  94. render: () => h('span'),
  95. }
  96. const App = {
  97. __scopeId: 'parent',
  98. render: () => {
  99. return h(
  100. Child,
  101. withCtx(() => {
  102. return [h('div'), h(Child2)]
  103. }),
  104. )
  105. },
  106. }
  107. const root = nodeOps.createElement('div')
  108. render(h(App), root)
  109. // slot content should have:
  110. // - scopeId from parent
  111. // - slotted scopeId (with `-s` postfix) from child (the tree owner)
  112. expect(serializeInner(root)).toBe(
  113. `<div child parent>` +
  114. `<div parent child-s></div>` +
  115. // component inside slot should have:
  116. // - scopeId from template context
  117. // - slotted scopeId from slot owner
  118. // - its own scopeId
  119. `<span child2 parent child-s></span>` +
  120. `</div>`,
  121. )
  122. })
  123. // #2892
  124. test(':slotted on forwarded slots', async () => {
  125. const Wrapper = {
  126. __scopeId: 'wrapper',
  127. render(this: any) {
  128. // <div class="wrapper"><slot/></div>
  129. return h('div', { class: 'wrapper' }, [
  130. renderSlot(
  131. this.$slots,
  132. 'default',
  133. {},
  134. undefined,
  135. true /* noSlotted */,
  136. ),
  137. ])
  138. },
  139. }
  140. const Slotted = {
  141. __scopeId: 'slotted',
  142. render(this: any) {
  143. // <Wrapper><slot/></Wrapper>
  144. return h(Wrapper, null, {
  145. default: withCtx(() => [renderSlot(this.$slots, 'default')]),
  146. })
  147. },
  148. }
  149. // simulate hoisted node
  150. pushScopeId('root')
  151. const hoisted = h('div', 'hoisted')
  152. popScopeId()
  153. const Root = {
  154. __scopeId: 'root',
  155. render(this: any) {
  156. // <Slotted><div>hoisted</div><div>{{ dynamic }}</div></Slotted>
  157. return h(Slotted, null, {
  158. default: withCtx(() => {
  159. return [hoisted, h('div', 'dynamic')]
  160. }),
  161. })
  162. },
  163. }
  164. const root = nodeOps.createElement('div')
  165. render(h(Root), root)
  166. expect(serializeInner(root)).toBe(
  167. `<div wrapper slotted root class="wrapper">` +
  168. `<div root slotted-s>hoisted</div>` +
  169. `<div root slotted-s>dynamic</div>` +
  170. `</div>`,
  171. )
  172. const Root2 = {
  173. __scopeId: 'root',
  174. render(this: any) {
  175. // <Slotted>
  176. // <Wrapper>
  177. // <div>hoisted</div><div>{{ dynamic }}</div>
  178. // </Wrapper>
  179. // </Slotted>
  180. return h(Slotted, null, {
  181. default: withCtx(() => [
  182. h(Wrapper, null, {
  183. default: withCtx(() => [hoisted, h('div', 'dynamic')]),
  184. }),
  185. ]),
  186. })
  187. },
  188. }
  189. const root2 = nodeOps.createElement('div')
  190. render(h(Root2), root2)
  191. expect(serializeInner(root2)).toBe(
  192. `<div wrapper slotted root class="wrapper">` +
  193. `<div wrapper root slotted-s class="wrapper">` +
  194. `<div root>hoisted</div>` +
  195. `<div root>dynamic</div>` +
  196. `</div>` +
  197. `</div>`,
  198. )
  199. })
  200. // #1988
  201. test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => {
  202. const App = {
  203. __scopeId: 'parent',
  204. render: () => {
  205. return h(Child)
  206. },
  207. }
  208. function Child() {
  209. return h(Child2, { class: 'foo' })
  210. }
  211. function Child2() {
  212. return h('div')
  213. }
  214. Child2.inheritAttrs = false
  215. const root = nodeOps.createElement('div')
  216. render(h(App), root)
  217. expect(serializeInner(root)).toBe(`<div parent></div>`)
  218. })
  219. test('should inherit scopeId through nested DEV_ROOT_FRAGMENT with inheritAttrs: false', async () => {
  220. const Parent = {
  221. __scopeId: 'parent',
  222. render() {
  223. return h(Child, { class: 'foo' })
  224. },
  225. }
  226. const ok = ref(true)
  227. const Child = defineComponent({
  228. inheritAttrs: false,
  229. render() {
  230. return (
  231. openBlock(),
  232. createBlock(
  233. Fragment,
  234. null,
  235. [
  236. createCommentVNode('comment1'),
  237. ok.value
  238. ? (openBlock(), createBlock('div', { key: 0 }, 'div1'))
  239. : (openBlock(),
  240. createBlock(
  241. Fragment,
  242. { key: 1 },
  243. [
  244. createCommentVNode('comment2'),
  245. createVNode('div', null, 'div2'),
  246. ],
  247. PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
  248. )),
  249. ],
  250. PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
  251. )
  252. )
  253. },
  254. })
  255. const root = nodeOps.createElement('div')
  256. render(h(Parent), root)
  257. expect(serializeInner(root)).toBe(`<!--comment1--><div parent>div1</div>`)
  258. ok.value = false
  259. await nextTick()
  260. expect(serializeInner(root)).toBe(
  261. `<!--comment1--><!--comment2--><div parent>div2</div>`,
  262. )
  263. })
  264. })
  265. describe('backwards compat with <=3.0.7', () => {
  266. const withParentId = withScopeId('parent')
  267. const withChildId = withScopeId('child')
  268. test('should attach scopeId', () => {
  269. const App = {
  270. __scopeId: 'parent',
  271. render: withParentId(() => {
  272. return h('div', [h('div')])
  273. }),
  274. }
  275. const root = nodeOps.createElement('div')
  276. render(h(App), root)
  277. expect(serializeInner(root)).toBe(`<div parent><div parent></div></div>`)
  278. })
  279. test('should attach scopeId to components in parent component', () => {
  280. const Child = {
  281. __scopeId: 'child',
  282. render: withChildId(() => {
  283. return h('div')
  284. }),
  285. }
  286. const App = {
  287. __scopeId: 'parent',
  288. render: withParentId(() => {
  289. return h('div', [h(Child)])
  290. }),
  291. }
  292. const root = nodeOps.createElement('div')
  293. render(h(App), root)
  294. expect(serializeInner(root)).toBe(
  295. `<div parent><div child parent></div></div>`,
  296. )
  297. })
  298. test('should work on slots', () => {
  299. const Child = {
  300. __scopeId: 'child',
  301. render: withChildId(function (this: any) {
  302. return h('div', renderSlot(this.$slots, 'default'))
  303. }),
  304. }
  305. const withChild2Id = withScopeId('child2')
  306. const Child2 = {
  307. __scopeId: 'child2',
  308. render: withChild2Id(() => h('span')),
  309. }
  310. const App = {
  311. __scopeId: 'parent',
  312. render: withParentId(() => {
  313. return h(
  314. Child,
  315. withParentId(() => {
  316. return [h('div'), h(Child2)]
  317. }),
  318. )
  319. }),
  320. }
  321. const root = nodeOps.createElement('div')
  322. render(h(App), root)
  323. // slot content should have:
  324. // - scopeId from parent
  325. // - slotted scopeId (with `-s` postfix) from child (the tree owner)
  326. expect(serializeInner(root)).toBe(
  327. `<div child parent>` +
  328. `<div parent child-s></div>` +
  329. // component inside slot should have:
  330. // - scopeId from template context
  331. // - slotted scopeId from slot owner
  332. // - its own scopeId
  333. `<span child2 parent child-s></span>` +
  334. `</div>`,
  335. )
  336. })
  337. // #1988
  338. test('should inherit scopeId through nested HOCs with inheritAttrs: false', () => {
  339. const withParentId = withScopeId('parent')
  340. const App = {
  341. __scopeId: 'parent',
  342. render: withParentId(() => {
  343. return h(Child)
  344. }),
  345. }
  346. function Child() {
  347. return h(Child2, { class: 'foo' })
  348. }
  349. function Child2() {
  350. return h('div')
  351. }
  352. Child2.inheritAttrs = false
  353. const root = nodeOps.createElement('div')
  354. render(h(App), root)
  355. expect(serializeInner(root)).toBe(`<div parent></div>`)
  356. })
  357. test('hoisted nodes', async () => {
  358. pushScopeId('foobar')
  359. const hoisted = h('div', 'hello')
  360. popScopeId()
  361. const App = {
  362. __scopeId: 'foobar',
  363. render: () => h('div', [hoisted]),
  364. }
  365. const root = nodeOps.createElement('div')
  366. render(h(App), root)
  367. expect(serializeInner(root)).toBe(
  368. `<div foobar><div foobar>hello</div></div>`,
  369. )
  370. })
  371. })