rendererAttrsFallthrough.spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. /**
  2. * @vitest-environment jsdom
  3. */
  4. // using DOM renderer because this case is mostly DOM-specific
  5. import {
  6. Fragment,
  7. type FunctionalComponent,
  8. Teleport,
  9. createBlock,
  10. createCommentVNode,
  11. createElementBlock,
  12. createElementVNode,
  13. defineComponent,
  14. h,
  15. mergeProps,
  16. nextTick,
  17. onUpdated,
  18. openBlock,
  19. ref,
  20. render,
  21. withModifiers,
  22. } from '@vue/runtime-dom'
  23. import { createApp } from 'vue'
  24. import { PatchFlags } from '@vue/shared'
  25. describe('attribute fallthrough', () => {
  26. it('should allow attrs to fallthrough', async () => {
  27. const click = vi.fn()
  28. const childUpdated = vi.fn()
  29. const Hello = {
  30. setup() {
  31. const count = ref(0)
  32. function inc() {
  33. count.value++
  34. click()
  35. }
  36. return () =>
  37. h(Child, {
  38. foo: count.value + 1,
  39. id: 'test',
  40. class: 'c' + count.value,
  41. style: { color: count.value ? 'red' : 'green' },
  42. onClick: inc,
  43. 'data-id': count.value + 1,
  44. })
  45. },
  46. }
  47. const Child = {
  48. setup(props: any) {
  49. onUpdated(childUpdated)
  50. return () =>
  51. h(
  52. 'div',
  53. {
  54. class: 'c2',
  55. style: { fontWeight: 'bold' },
  56. },
  57. props.foo,
  58. )
  59. },
  60. }
  61. const root = document.createElement('div')
  62. document.body.appendChild(root)
  63. render(h(Hello), root)
  64. const node = root.children[0] as HTMLElement
  65. expect(node.getAttribute('id')).toBe('test')
  66. expect(node.getAttribute('foo')).toBe('1')
  67. expect(node.getAttribute('class')).toBe('c2 c0')
  68. expect(node.style.color).toBe('green')
  69. expect(node.style.fontWeight).toBe('bold')
  70. expect(node.dataset.id).toBe('1')
  71. node.dispatchEvent(new CustomEvent('click'))
  72. expect(click).toHaveBeenCalled()
  73. await nextTick()
  74. expect(childUpdated).toHaveBeenCalled()
  75. expect(node.getAttribute('id')).toBe('test')
  76. expect(node.getAttribute('foo')).toBe('2')
  77. expect(node.getAttribute('class')).toBe('c2 c1')
  78. expect(node.style.color).toBe('red')
  79. expect(node.style.fontWeight).toBe('bold')
  80. expect(node.dataset.id).toBe('2')
  81. })
  82. it('should only allow whitelisted fallthrough on functional component with optional props', async () => {
  83. const click = vi.fn()
  84. const childUpdated = vi.fn()
  85. const count = ref(0)
  86. function inc() {
  87. count.value++
  88. click()
  89. }
  90. const Hello = () =>
  91. h(Child, {
  92. foo: count.value + 1,
  93. id: 'test',
  94. class: 'c' + count.value,
  95. style: { color: count.value ? 'red' : 'green' },
  96. onClick: inc,
  97. })
  98. const Child = (props: any) => {
  99. childUpdated()
  100. return h(
  101. 'div',
  102. {
  103. class: 'c2',
  104. style: { fontWeight: 'bold' },
  105. },
  106. props.foo,
  107. )
  108. }
  109. const root = document.createElement('div')
  110. document.body.appendChild(root)
  111. render(h(Hello), root)
  112. const node = root.children[0] as HTMLElement
  113. // not whitelisted
  114. expect(node.getAttribute('id')).toBe(null)
  115. expect(node.getAttribute('foo')).toBe(null)
  116. // whitelisted: style, class, event listeners
  117. expect(node.getAttribute('class')).toBe('c2 c0')
  118. expect(node.style.color).toBe('green')
  119. expect(node.style.fontWeight).toBe('bold')
  120. node.dispatchEvent(new CustomEvent('click'))
  121. expect(click).toHaveBeenCalled()
  122. await nextTick()
  123. expect(childUpdated).toHaveBeenCalled()
  124. expect(node.getAttribute('id')).toBe(null)
  125. expect(node.getAttribute('foo')).toBe(null)
  126. expect(node.getAttribute('class')).toBe('c2 c1')
  127. expect(node.style.color).toBe('red')
  128. expect(node.style.fontWeight).toBe('bold')
  129. })
  130. it('should allow all attrs on functional component with declared props', async () => {
  131. const click = vi.fn()
  132. const childUpdated = vi.fn()
  133. const count = ref(0)
  134. function inc() {
  135. count.value++
  136. click()
  137. }
  138. const Hello = () =>
  139. h(Child, {
  140. foo: count.value + 1,
  141. id: 'test',
  142. class: 'c' + count.value,
  143. style: { color: count.value ? 'red' : 'green' },
  144. onClick: inc,
  145. })
  146. const Child = (props: { foo: number }) => {
  147. childUpdated()
  148. return h(
  149. 'div',
  150. {
  151. class: 'c2',
  152. style: { fontWeight: 'bold' },
  153. },
  154. props.foo,
  155. )
  156. }
  157. Child.props = ['foo']
  158. const root = document.createElement('div')
  159. document.body.appendChild(root)
  160. render(h(Hello), root)
  161. const node = root.children[0] as HTMLElement
  162. expect(node.getAttribute('id')).toBe('test')
  163. expect(node.getAttribute('foo')).toBe(null) // declared as prop
  164. expect(node.getAttribute('class')).toBe('c2 c0')
  165. expect(node.style.color).toBe('green')
  166. expect(node.style.fontWeight).toBe('bold')
  167. node.dispatchEvent(new CustomEvent('click'))
  168. expect(click).toHaveBeenCalled()
  169. await nextTick()
  170. expect(childUpdated).toHaveBeenCalled()
  171. expect(node.getAttribute('id')).toBe('test')
  172. expect(node.getAttribute('foo')).toBe(null)
  173. expect(node.getAttribute('class')).toBe('c2 c1')
  174. expect(node.style.color).toBe('red')
  175. expect(node.style.fontWeight).toBe('bold')
  176. })
  177. it('should fallthrough for nested components', async () => {
  178. const click = vi.fn()
  179. const childUpdated = vi.fn()
  180. const grandChildUpdated = vi.fn()
  181. const Hello = {
  182. setup() {
  183. const count = ref(0)
  184. function inc() {
  185. count.value++
  186. click()
  187. }
  188. return () =>
  189. h(Child, {
  190. foo: 1,
  191. id: 'test',
  192. class: 'c' + count.value,
  193. style: { color: count.value ? 'red' : 'green' },
  194. onClick: inc,
  195. })
  196. },
  197. }
  198. const Child = {
  199. setup(props: any) {
  200. onUpdated(childUpdated)
  201. // HOC simply passing props down.
  202. // this will result in merging the same attrs, but should be deduped by
  203. // `mergeProps`.
  204. return () => h(GrandChild, props)
  205. },
  206. }
  207. const GrandChild = defineComponent({
  208. props: {
  209. id: String,
  210. foo: Number,
  211. },
  212. setup(props) {
  213. onUpdated(grandChildUpdated)
  214. return () =>
  215. h(
  216. 'div',
  217. {
  218. id: props.id,
  219. class: 'c2',
  220. style: { fontWeight: 'bold' },
  221. },
  222. props.foo,
  223. )
  224. },
  225. })
  226. const root = document.createElement('div')
  227. document.body.appendChild(root)
  228. render(h(Hello), root)
  229. const node = root.children[0] as HTMLElement
  230. // with declared props, any parent attr that isn't a prop falls through
  231. expect(node.getAttribute('id')).toBe('test')
  232. expect(node.getAttribute('class')).toBe('c2 c0')
  233. expect(node.style.color).toBe('green')
  234. expect(node.style.fontWeight).toBe('bold')
  235. node.dispatchEvent(new CustomEvent('click'))
  236. expect(click).toHaveBeenCalled()
  237. // ...while declared ones remain props
  238. expect(node.hasAttribute('foo')).toBe(false)
  239. await nextTick()
  240. expect(childUpdated).toHaveBeenCalled()
  241. expect(grandChildUpdated).toHaveBeenCalled()
  242. expect(node.getAttribute('id')).toBe('test')
  243. expect(node.getAttribute('class')).toBe('c2 c1')
  244. expect(node.style.color).toBe('red')
  245. expect(node.style.fontWeight).toBe('bold')
  246. expect(node.hasAttribute('foo')).toBe(false)
  247. })
  248. it('should not fallthrough with inheritAttrs: false', () => {
  249. const Parent = {
  250. render() {
  251. return h(Child, { foo: 1, class: 'parent' })
  252. },
  253. }
  254. const Child = defineComponent({
  255. props: ['foo'],
  256. inheritAttrs: false,
  257. render() {
  258. return h('div', this.foo)
  259. },
  260. })
  261. const root = document.createElement('div')
  262. document.body.appendChild(root)
  263. render(h(Parent), root)
  264. // should not contain class
  265. expect(root.innerHTML).toMatch(`<div>1</div>`)
  266. })
  267. // #3741
  268. it('should not fallthrough with inheritAttrs: false from mixins', () => {
  269. const Parent = {
  270. render() {
  271. return h(Child, { foo: 1, class: 'parent' })
  272. },
  273. }
  274. const mixin = {
  275. inheritAttrs: false,
  276. }
  277. const Child = defineComponent({
  278. mixins: [mixin],
  279. props: ['foo'],
  280. render() {
  281. return h('div', this.foo)
  282. },
  283. })
  284. const root = document.createElement('div')
  285. document.body.appendChild(root)
  286. render(h(Parent), root)
  287. // should not contain class
  288. expect(root.innerHTML).toMatch(`<div>1</div>`)
  289. })
  290. it('explicit spreading with inheritAttrs: false', () => {
  291. const Parent = {
  292. render() {
  293. return h(Child, { foo: 1, class: 'parent' })
  294. },
  295. }
  296. const Child = defineComponent({
  297. props: ['foo'],
  298. inheritAttrs: false,
  299. render() {
  300. return h(
  301. 'div',
  302. mergeProps(
  303. {
  304. class: 'child',
  305. },
  306. this.$attrs,
  307. ),
  308. this.foo,
  309. )
  310. },
  311. })
  312. const root = document.createElement('div')
  313. document.body.appendChild(root)
  314. render(h(Parent), root)
  315. // should merge parent/child classes
  316. expect(root.innerHTML).toMatch(`<div class="child parent">1</div>`)
  317. })
  318. it('should warn when fallthrough fails on non-single-root', () => {
  319. const Parent = {
  320. render() {
  321. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  322. },
  323. }
  324. const Child = defineComponent({
  325. props: ['foo'],
  326. render() {
  327. return [h('div'), h('div')]
  328. },
  329. })
  330. const root = document.createElement('div')
  331. document.body.appendChild(root)
  332. render(h(Parent), root)
  333. expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
  334. expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
  335. })
  336. it('should warn when fallthrough fails on teleport root node', () => {
  337. const Parent = {
  338. render() {
  339. return h(Child, { class: 'parent' })
  340. },
  341. }
  342. const root = document.createElement('div')
  343. const Child = defineComponent({
  344. render() {
  345. return h(Teleport, { to: root }, h('div'))
  346. },
  347. })
  348. document.body.appendChild(root)
  349. render(h(Parent), root)
  350. expect(`Extraneous non-props attributes (class)`).toHaveBeenWarned()
  351. })
  352. it('should dedupe same listeners when $attrs is used during render', () => {
  353. const click = vi.fn()
  354. const count = ref(0)
  355. function inc() {
  356. count.value++
  357. click()
  358. }
  359. const Parent = {
  360. render() {
  361. return h(Child, { onClick: inc })
  362. },
  363. }
  364. const Child = defineComponent({
  365. render() {
  366. return h(
  367. 'div',
  368. mergeProps(
  369. {
  370. onClick: withModifiers(() => {}, ['prevent', 'stop']),
  371. },
  372. this.$attrs,
  373. ),
  374. )
  375. },
  376. })
  377. const root = document.createElement('div')
  378. document.body.appendChild(root)
  379. render(h(Parent), root)
  380. const node = root.children[0] as HTMLElement
  381. node.dispatchEvent(new CustomEvent('click'))
  382. expect(click).toHaveBeenCalledTimes(1)
  383. expect(count.value).toBe(1)
  384. })
  385. it('should not warn when $attrs is used during render', () => {
  386. const Parent = {
  387. render() {
  388. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  389. },
  390. }
  391. const Child = defineComponent({
  392. props: ['foo'],
  393. render() {
  394. return [h('div'), h('div', this.$attrs)]
  395. },
  396. })
  397. const root = document.createElement('div')
  398. document.body.appendChild(root)
  399. render(h(Parent), root)
  400. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  401. expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
  402. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  403. })
  404. it('should not warn when context.attrs is used during render', () => {
  405. const Parent = {
  406. render() {
  407. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  408. },
  409. }
  410. const Child = defineComponent({
  411. props: ['foo'],
  412. setup(_props, { attrs }) {
  413. return () => [h('div'), h('div', attrs)]
  414. },
  415. })
  416. const root = document.createElement('div')
  417. document.body.appendChild(root)
  418. render(h(Parent), root)
  419. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  420. expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
  421. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  422. })
  423. it('should not warn when context.attrs is used during render (functional)', () => {
  424. const Parent = {
  425. render() {
  426. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  427. },
  428. }
  429. const Child: FunctionalComponent = (_, { attrs }) => [
  430. h('div'),
  431. h('div', attrs),
  432. ]
  433. Child.props = ['foo']
  434. const root = document.createElement('div')
  435. document.body.appendChild(root)
  436. render(h(Parent), root)
  437. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  438. expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
  439. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  440. })
  441. it('should not warn when functional component has optional props', () => {
  442. const Parent = {
  443. render() {
  444. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  445. },
  446. }
  447. const Child = (props: any) => [h('div'), h('div', { class: props.class })]
  448. const root = document.createElement('div')
  449. document.body.appendChild(root)
  450. render(h(Parent), root)
  451. expect(`Extraneous non-props attributes`).not.toHaveBeenWarned()
  452. expect(`Extraneous non-emits event listeners`).not.toHaveBeenWarned()
  453. expect(root.innerHTML).toBe(`<div></div><div class="parent"></div>`)
  454. })
  455. it('should warn when functional component has props and does not use attrs', () => {
  456. const Parent = {
  457. render() {
  458. return h(Child, { foo: 1, class: 'parent', onBar: () => {} })
  459. },
  460. }
  461. const Child: FunctionalComponent = () => [h('div'), h('div')]
  462. Child.props = ['foo']
  463. const root = document.createElement('div')
  464. document.body.appendChild(root)
  465. render(h(Parent), root)
  466. expect(`Extraneous non-props attributes`).toHaveBeenWarned()
  467. expect(`Extraneous non-emits event listeners`).toHaveBeenWarned()
  468. expect(root.innerHTML).toBe(`<div></div><div></div>`)
  469. })
  470. // #677
  471. it('should update merged dynamic attrs on optimized child root', async () => {
  472. const aria = ref('true')
  473. const cls = ref('bar')
  474. const Parent = {
  475. render() {
  476. return h(Child, { 'aria-hidden': aria.value, class: cls.value })
  477. },
  478. }
  479. const Child = {
  480. props: [],
  481. render() {
  482. return (openBlock(), createBlock('div'))
  483. },
  484. }
  485. const root = document.createElement('div')
  486. document.body.appendChild(root)
  487. render(h(Parent), root)
  488. expect(root.innerHTML).toBe(`<div aria-hidden="true" class="bar"></div>`)
  489. aria.value = 'false'
  490. await nextTick()
  491. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="bar"></div>`)
  492. cls.value = 'barr'
  493. await nextTick()
  494. expect(root.innerHTML).toBe(`<div aria-hidden="false" class="barr"></div>`)
  495. })
  496. it('should not let listener fallthrough when declared in emits (stateful)', () => {
  497. const Child = defineComponent({
  498. emits: ['click'],
  499. render() {
  500. return h(
  501. 'button',
  502. {
  503. onClick: () => {
  504. this.$emit('click', 'custom')
  505. },
  506. },
  507. 'hello',
  508. )
  509. },
  510. })
  511. const onClick = vi.fn()
  512. const App = {
  513. render() {
  514. return h(Child, {
  515. onClick,
  516. })
  517. },
  518. }
  519. const root = document.createElement('div')
  520. document.body.appendChild(root)
  521. render(h(App), root)
  522. const node = root.children[0] as HTMLElement
  523. node.dispatchEvent(new CustomEvent('click'))
  524. expect(onClick).toHaveBeenCalledTimes(1)
  525. expect(onClick).toHaveBeenCalledWith('custom')
  526. })
  527. it('should not let listener fallthrough when declared in emits (functional)', () => {
  528. const Child: FunctionalComponent<{}, { click: any }> = (_, { emit }) => {
  529. // should not be in props
  530. expect((_ as any).onClick).toBeUndefined()
  531. return h('button', {
  532. onClick: () => {
  533. emit('click', 'custom')
  534. },
  535. })
  536. }
  537. Child.emits = ['click']
  538. const onClick = vi.fn()
  539. const App = {
  540. render() {
  541. return h(Child, {
  542. onClick,
  543. })
  544. },
  545. }
  546. const root = document.createElement('div')
  547. document.body.appendChild(root)
  548. render(h(App), root)
  549. const node = root.children[0] as HTMLElement
  550. node.dispatchEvent(new CustomEvent('click'))
  551. expect(onClick).toHaveBeenCalledTimes(1)
  552. expect(onClick).toHaveBeenCalledWith('custom')
  553. })
  554. it('should support fallthrough for fragments with single element + comments', () => {
  555. const click = vi.fn()
  556. const Hello = {
  557. setup() {
  558. return () => h(Child, { class: 'foo', onClick: click })
  559. },
  560. }
  561. const Child = {
  562. setup() {
  563. return () => (
  564. openBlock(),
  565. createBlock(
  566. Fragment,
  567. null,
  568. [
  569. createCommentVNode('hello'),
  570. h('button'),
  571. createCommentVNode('world'),
  572. ],
  573. PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
  574. )
  575. )
  576. },
  577. }
  578. const root = document.createElement('div')
  579. document.body.appendChild(root)
  580. render(h(Hello), root)
  581. expect(root.innerHTML).toBe(
  582. `<!--hello--><button class="foo"></button><!--world-->`,
  583. )
  584. const button = root.children[0] as HTMLElement
  585. button.dispatchEvent(new CustomEvent('click'))
  586. expect(click).toHaveBeenCalled()
  587. })
  588. it('should support fallthrough for nested dev root fragments', async () => {
  589. const toggle = ref(false)
  590. const Child = {
  591. setup() {
  592. return () => (
  593. openBlock(),
  594. createElementBlock(
  595. Fragment,
  596. null,
  597. [
  598. createCommentVNode(' comment A '),
  599. toggle.value
  600. ? (openBlock(), createElementBlock('span', { key: 0 }, 'Foo'))
  601. : (openBlock(),
  602. createElementBlock(
  603. Fragment,
  604. { key: 1 },
  605. [
  606. createCommentVNode(' comment B '),
  607. createElementVNode('div', null, 'Bar'),
  608. ],
  609. PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
  610. )),
  611. ],
  612. PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
  613. )
  614. )
  615. },
  616. }
  617. const Root = {
  618. setup() {
  619. return () => (openBlock(), createBlock(Child, { class: 'red' }))
  620. },
  621. }
  622. const root = document.createElement('div')
  623. document.body.appendChild(root)
  624. render(h(Root), root)
  625. expect(root.innerHTML).toBe(
  626. `<!-- comment A --><!-- comment B --><div class="red">Bar</div>`,
  627. )
  628. toggle.value = true
  629. await nextTick()
  630. expect(root.innerHTML).toBe(
  631. `<!-- comment A --><span class=\"red\">Foo</span>`,
  632. )
  633. })
  634. // #1989
  635. it('should not fallthrough v-model listeners with corresponding declared prop', () => {
  636. let textFoo = ''
  637. let textBar = ''
  638. const click = vi.fn()
  639. const App = defineComponent({
  640. setup() {
  641. return () =>
  642. h(Child, {
  643. modelValue: textFoo,
  644. 'onUpdate:modelValue': (val: string) => {
  645. textFoo = val
  646. },
  647. })
  648. },
  649. })
  650. const Child = defineComponent({
  651. props: ['modelValue'],
  652. setup(_props, { emit }) {
  653. return () =>
  654. h(GrandChild, {
  655. modelValue: textBar,
  656. 'onUpdate:modelValue': (val: string) => {
  657. textBar = val
  658. emit('update:modelValue', 'from Child')
  659. },
  660. })
  661. },
  662. })
  663. const GrandChild = defineComponent({
  664. props: ['modelValue'],
  665. setup(_props, { emit }) {
  666. return () =>
  667. h('button', {
  668. onClick() {
  669. click()
  670. emit('update:modelValue', 'from GrandChild')
  671. },
  672. })
  673. },
  674. })
  675. const root = document.createElement('div')
  676. document.body.appendChild(root)
  677. render(h(App), root)
  678. const node = root.children[0] as HTMLElement
  679. node.dispatchEvent(new CustomEvent('click'))
  680. expect(click).toHaveBeenCalled()
  681. expect(textBar).toBe('from GrandChild')
  682. expect(textFoo).toBe('from Child')
  683. })
  684. // covers uncaught regression #10710
  685. it('should track this.$attrs access in slots', async () => {
  686. const GrandChild = {
  687. template: `<slot/>`,
  688. }
  689. const Child = {
  690. components: { GrandChild },
  691. template: `<div><GrandChild>{{ $attrs.foo }}</GrandChild></div>`,
  692. }
  693. const obj = ref(1)
  694. const App = {
  695. render() {
  696. return h(Child, { foo: obj.value })
  697. },
  698. }
  699. const root = document.createElement('div')
  700. createApp(App).mount(root)
  701. expect(root.innerHTML).toBe('<div foo="1">1</div>')
  702. obj.value = 2
  703. await nextTick()
  704. expect(root.innerHTML).toBe('<div foo="2">2</div>')
  705. })
  706. })