customElement.spec.ts 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  1. import type { MockedFunction } from 'vitest'
  2. import {
  3. type HMRRuntime,
  4. type Ref,
  5. type VueElement,
  6. createApp,
  7. defineAsyncComponent,
  8. defineComponent,
  9. defineCustomElement,
  10. h,
  11. inject,
  12. nextTick,
  13. provide,
  14. ref,
  15. render,
  16. renderSlot,
  17. useHost,
  18. useShadowRoot,
  19. } from '../src'
  20. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  21. describe('defineCustomElement', () => {
  22. const container = document.createElement('div')
  23. document.body.appendChild(container)
  24. beforeEach(() => {
  25. container.innerHTML = ''
  26. })
  27. describe('mounting/unmount', () => {
  28. const E = defineCustomElement({
  29. props: {
  30. msg: {
  31. type: String,
  32. default: 'hello',
  33. },
  34. },
  35. render() {
  36. return h('div', this.msg)
  37. },
  38. })
  39. customElements.define('my-element', E)
  40. test('should work', () => {
  41. container.innerHTML = `<my-element></my-element>`
  42. const e = container.childNodes[0] as VueElement
  43. expect(e).toBeInstanceOf(E)
  44. expect(e._instance).toBeTruthy()
  45. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  46. })
  47. test('should work w/ manual instantiation', () => {
  48. const e = new E({ msg: 'inline' })
  49. // should lazy init
  50. expect(e._instance).toBe(null)
  51. // should initialize on connect
  52. container.appendChild(e)
  53. expect(e._instance).toBeTruthy()
  54. expect(e.shadowRoot!.innerHTML).toBe(`<div>inline</div>`)
  55. })
  56. test('should unmount on remove', async () => {
  57. container.innerHTML = `<my-element></my-element>`
  58. const e = container.childNodes[0] as VueElement
  59. container.removeChild(e)
  60. await nextTick()
  61. expect(e._instance).toBe(null)
  62. expect(e.shadowRoot!.innerHTML).toBe('')
  63. })
  64. // #10610
  65. test('When elements move, avoid prematurely disconnecting MutationObserver', async () => {
  66. const CustomInput = defineCustomElement({
  67. props: ['value'],
  68. emits: ['update'],
  69. setup(props, { emit }) {
  70. return () =>
  71. h('input', {
  72. type: 'number',
  73. value: props.value,
  74. onInput: (e: InputEvent) => {
  75. const num = (e.target! as HTMLInputElement).valueAsNumber
  76. emit('update', Number.isNaN(num) ? null : num)
  77. },
  78. })
  79. },
  80. })
  81. customElements.define('my-el-input', CustomInput)
  82. const num = ref('12')
  83. const containerComp = defineComponent({
  84. setup() {
  85. return () => {
  86. return h('div', [
  87. h('my-el-input', {
  88. value: num.value,
  89. onUpdate: ($event: CustomEvent) => {
  90. num.value = $event.detail[0]
  91. },
  92. }),
  93. h('div', { id: 'move' }),
  94. ])
  95. }
  96. },
  97. })
  98. const app = createApp(containerComp)
  99. const container = document.createElement('div')
  100. document.body.appendChild(container)
  101. app.mount(container)
  102. const myInputEl = container.querySelector('my-el-input')!
  103. const inputEl = myInputEl.shadowRoot!.querySelector('input')!
  104. await nextTick()
  105. expect(inputEl.value).toBe('12')
  106. const moveEl = container.querySelector('#move')!
  107. moveEl.append(myInputEl)
  108. await nextTick()
  109. myInputEl.removeAttribute('value')
  110. await nextTick()
  111. expect(inputEl.value).toBe('')
  112. })
  113. test('should not unmount on move', async () => {
  114. container.innerHTML = `<div><my-element></my-element></div>`
  115. const e = container.childNodes[0].childNodes[0] as VueElement
  116. const i = e._instance
  117. // moving from one parent to another - this will trigger both disconnect
  118. // and connected callbacks synchronously
  119. container.appendChild(e)
  120. await nextTick()
  121. // should be the same instance
  122. expect(e._instance).toBe(i)
  123. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  124. })
  125. test('remove then insert again', async () => {
  126. container.innerHTML = `<my-element></my-element>`
  127. const e = container.childNodes[0] as VueElement
  128. container.removeChild(e)
  129. await nextTick()
  130. expect(e._instance).toBe(null)
  131. expect(e.shadowRoot!.innerHTML).toBe('')
  132. container.appendChild(e)
  133. expect(e._instance).toBeTruthy()
  134. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  135. })
  136. })
  137. describe('props', () => {
  138. const E = defineCustomElement({
  139. props: {
  140. foo: [String, null],
  141. bar: Object,
  142. bazQux: null,
  143. value: null,
  144. },
  145. render() {
  146. return [
  147. h('div', null, this.foo || ''),
  148. h('div', null, this.bazQux || (this.bar && this.bar.x)),
  149. ]
  150. },
  151. })
  152. customElements.define('my-el-props', E)
  153. test('renders custom element w/ correct object prop value', () => {
  154. render(h('my-el-props', { value: { x: 1 } }), container)
  155. const el = container.children[0]
  156. expect((el as any).value).toEqual({ x: 1 })
  157. })
  158. test('props via attribute', async () => {
  159. // bazQux should map to `baz-qux` attribute
  160. container.innerHTML = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
  161. const e = container.childNodes[0] as VueElement
  162. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
  163. // change attr
  164. e.setAttribute('foo', 'changed')
  165. await nextTick()
  166. expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div><div>bye</div>')
  167. e.setAttribute('baz-qux', 'changed')
  168. await nextTick()
  169. expect(e.shadowRoot!.innerHTML).toBe(
  170. '<div>changed</div><div>changed</div>',
  171. )
  172. })
  173. test('props via properties', async () => {
  174. const e = new E()
  175. e.foo = 'one'
  176. e.bar = { x: 'two' }
  177. container.appendChild(e)
  178. expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
  179. // reflect
  180. // should reflect primitive value
  181. expect(e.getAttribute('foo')).toBe('one')
  182. // should not reflect rich data
  183. expect(e.hasAttribute('bar')).toBe(false)
  184. e.foo = 'three'
  185. await nextTick()
  186. expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
  187. expect(e.getAttribute('foo')).toBe('three')
  188. e.foo = null
  189. await nextTick()
  190. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
  191. expect(e.hasAttribute('foo')).toBe(false)
  192. e.foo = undefined
  193. await nextTick()
  194. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
  195. expect(e.hasAttribute('foo')).toBe(false)
  196. expect(e.foo).toBe(undefined)
  197. e.bazQux = 'four'
  198. await nextTick()
  199. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
  200. expect(e.getAttribute('baz-qux')).toBe('four')
  201. })
  202. test('attribute -> prop type casting', async () => {
  203. const E = defineCustomElement({
  204. props: {
  205. fooBar: Number, // test casting of camelCase prop names
  206. bar: Boolean,
  207. baz: String,
  208. },
  209. render() {
  210. return [
  211. this.fooBar,
  212. typeof this.fooBar,
  213. this.bar,
  214. typeof this.bar,
  215. this.baz,
  216. typeof this.baz,
  217. ].join(' ')
  218. },
  219. })
  220. customElements.define('my-el-props-cast', E)
  221. container.innerHTML = `<my-el-props-cast foo-bar="1" baz="12345"></my-el-props-cast>`
  222. const e = container.childNodes[0] as VueElement
  223. expect(e.shadowRoot!.innerHTML).toBe(
  224. `1 number false boolean 12345 string`,
  225. )
  226. e.setAttribute('bar', '')
  227. await nextTick()
  228. expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
  229. e.setAttribute('foo-bar', '2e1')
  230. await nextTick()
  231. expect(e.shadowRoot!.innerHTML).toBe(
  232. `20 number true boolean 12345 string`,
  233. )
  234. e.setAttribute('baz', '2e1')
  235. await nextTick()
  236. expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
  237. })
  238. // #4772
  239. test('attr casting w/ programmatic creation', () => {
  240. const E = defineCustomElement({
  241. props: {
  242. foo: Number,
  243. },
  244. render() {
  245. return `foo type: ${typeof this.foo}`
  246. },
  247. })
  248. customElements.define('my-element-programmatic', E)
  249. const el = document.createElement('my-element-programmatic') as any
  250. el.setAttribute('foo', '123')
  251. container.appendChild(el)
  252. expect(el.shadowRoot.innerHTML).toBe(`foo type: number`)
  253. })
  254. test('handling properties set before upgrading', () => {
  255. const E = defineCustomElement({
  256. props: {
  257. foo: String,
  258. dataAge: Number,
  259. },
  260. setup(props) {
  261. expect(props.foo).toBe('hello')
  262. expect(props.dataAge).toBe(5)
  263. },
  264. render() {
  265. return h('div', `foo: ${this.foo}`)
  266. },
  267. })
  268. const el = document.createElement('my-el-upgrade') as any
  269. el.foo = 'hello'
  270. el.dataset.age = 5
  271. el.notProp = 1
  272. container.appendChild(el)
  273. customElements.define('my-el-upgrade', E)
  274. expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`)
  275. // should not reflect if not declared as a prop
  276. expect(el.hasAttribute('not-prop')).toBe(false)
  277. })
  278. test('handle properties set before connecting', () => {
  279. const obj = { a: 1 }
  280. const E = defineCustomElement({
  281. props: {
  282. foo: String,
  283. post: Object,
  284. },
  285. setup(props) {
  286. expect(props.foo).toBe('hello')
  287. expect(props.post).toBe(obj)
  288. },
  289. render() {
  290. return JSON.stringify(this.post)
  291. },
  292. })
  293. customElements.define('my-el-preconnect', E)
  294. const el = document.createElement('my-el-preconnect') as any
  295. el.foo = 'hello'
  296. el.post = obj
  297. container.appendChild(el)
  298. expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj))
  299. })
  300. // https://github.com/vuejs/core/issues/6163
  301. test('handle components with no props', async () => {
  302. const E = defineCustomElement({
  303. render() {
  304. return h('div', 'foo')
  305. },
  306. })
  307. customElements.define('my-element-noprops', E)
  308. const el = document.createElement('my-element-noprops')
  309. container.appendChild(el)
  310. await nextTick()
  311. expect(el.shadowRoot!.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
  312. })
  313. // #5793
  314. test('set number value in dom property', () => {
  315. const E = defineCustomElement({
  316. props: {
  317. 'max-age': Number,
  318. },
  319. render() {
  320. // @ts-expect-error
  321. return `max age: ${this.maxAge}/type: ${typeof this.maxAge}`
  322. },
  323. })
  324. customElements.define('my-element-number-property', E)
  325. const el = document.createElement('my-element-number-property') as any
  326. container.appendChild(el)
  327. el.maxAge = 50
  328. expect(el.maxAge).toBe(50)
  329. expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number')
  330. })
  331. // #9006
  332. test('should reflect default value', () => {
  333. const E = defineCustomElement({
  334. props: {
  335. value: {
  336. type: String,
  337. default: 'hi',
  338. },
  339. },
  340. render() {
  341. return this.value
  342. },
  343. })
  344. customElements.define('my-el-default-val', E)
  345. container.innerHTML = `<my-el-default-val></my-el-default-val>`
  346. const e = container.childNodes[0] as any
  347. expect(e.value).toBe('hi')
  348. })
  349. test('support direct setup function syntax with extra options', () => {
  350. const E = defineCustomElement(
  351. props => {
  352. return () => props.text
  353. },
  354. {
  355. props: {
  356. text: String,
  357. },
  358. },
  359. )
  360. customElements.define('my-el-setup-with-props', E)
  361. container.innerHTML = `<my-el-setup-with-props text="hello"></my-el-setup-with-props>`
  362. const e = container.childNodes[0] as VueElement
  363. expect(e.shadowRoot!.innerHTML).toBe('hello')
  364. })
  365. })
  366. describe('attrs', () => {
  367. const E = defineCustomElement({
  368. render() {
  369. return [h('div', null, this.$attrs.foo as string)]
  370. },
  371. })
  372. customElements.define('my-el-attrs', E)
  373. test('attrs via attribute', async () => {
  374. container.innerHTML = `<my-el-attrs foo="hello"></my-el-attrs>`
  375. const e = container.childNodes[0] as VueElement
  376. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  377. e.setAttribute('foo', 'changed')
  378. await nextTick()
  379. expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div>')
  380. })
  381. test('non-declared properties should not show up in $attrs', () => {
  382. const e = new E()
  383. // @ts-expect-error
  384. e.foo = '123'
  385. container.appendChild(e)
  386. expect(e.shadowRoot!.innerHTML).toBe('<div></div>')
  387. })
  388. })
  389. describe('emits', () => {
  390. const CompDef = defineComponent({
  391. setup(_, { emit }) {
  392. emit('created')
  393. return () =>
  394. h('div', {
  395. onClick: () => {
  396. emit('my-click', 1)
  397. },
  398. onMousedown: () => {
  399. emit('myEvent', 1) // validate hyphenation
  400. },
  401. onWheel: () => {
  402. emit('my-wheel', { bubbles: true }, 1)
  403. },
  404. })
  405. },
  406. })
  407. const E = defineCustomElement(CompDef)
  408. customElements.define('my-el-emits', E)
  409. test('emit on connect', () => {
  410. const e = new E()
  411. const spy = vi.fn()
  412. e.addEventListener('created', spy)
  413. container.appendChild(e)
  414. expect(spy).toHaveBeenCalled()
  415. })
  416. test('emit on interaction', () => {
  417. container.innerHTML = `<my-el-emits></my-el-emits>`
  418. const e = container.childNodes[0] as VueElement
  419. const spy = vi.fn()
  420. e.addEventListener('my-click', spy)
  421. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  422. expect(spy).toHaveBeenCalledTimes(1)
  423. expect(spy.mock.calls[0][0]).toMatchObject({
  424. detail: [1],
  425. })
  426. })
  427. // #5373
  428. test('case transform for camelCase event', () => {
  429. container.innerHTML = `<my-el-emits></my-el-emits>`
  430. const e = container.childNodes[0] as VueElement
  431. const spy1 = vi.fn()
  432. e.addEventListener('myEvent', spy1)
  433. const spy2 = vi.fn()
  434. // emitting myEvent, but listening for my-event. This happens when
  435. // using the custom element in a Vue template
  436. e.addEventListener('my-event', spy2)
  437. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('mousedown'))
  438. expect(spy1).toHaveBeenCalledTimes(1)
  439. expect(spy2).toHaveBeenCalledTimes(1)
  440. })
  441. test('emit from within async component wrapper', async () => {
  442. const p = new Promise<typeof CompDef>(res => res(CompDef as any))
  443. const E = defineCustomElement(defineAsyncComponent(() => p))
  444. customElements.define('my-async-el-emits', E)
  445. container.innerHTML = `<my-async-el-emits></my-async-el-emits>`
  446. const e = container.childNodes[0] as VueElement
  447. const spy = vi.fn()
  448. e.addEventListener('my-click', spy)
  449. // this feels brittle but seems necessary to reach the node in the DOM.
  450. await customElements.whenDefined('my-async-el-emits')
  451. await nextTick()
  452. await nextTick()
  453. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  454. expect(spy).toHaveBeenCalled()
  455. expect(spy.mock.calls[0][0]).toMatchObject({
  456. detail: [1],
  457. })
  458. })
  459. // #7293
  460. test('emit in an async component wrapper with properties bound', async () => {
  461. const E = defineCustomElement(
  462. defineAsyncComponent(
  463. () => new Promise<typeof CompDef>(res => res(CompDef as any)),
  464. ),
  465. )
  466. customElements.define('my-async-el-props-emits', E)
  467. container.innerHTML = `<my-async-el-props-emits id="my_async_el_props_emits"></my-async-el-props-emits>`
  468. const e = container.childNodes[0] as VueElement
  469. const spy = vi.fn()
  470. e.addEventListener('my-click', spy)
  471. await customElements.whenDefined('my-async-el-props-emits')
  472. await nextTick()
  473. await nextTick()
  474. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  475. expect(spy).toHaveBeenCalled()
  476. expect(spy.mock.calls[0][0]).toMatchObject({
  477. detail: [1],
  478. })
  479. })
  480. test('emit with options', async () => {
  481. container.innerHTML = `<my-el-emits></my-el-emits>`
  482. const e = container.childNodes[0] as VueElement
  483. const spy = vi.fn()
  484. e.addEventListener('my-wheel', spy)
  485. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('wheel'))
  486. expect(spy).toHaveBeenCalledTimes(1)
  487. expect(spy.mock.calls[0][0]).toMatchObject({
  488. bubbles: true,
  489. detail: [{ bubbles: true }, 1],
  490. })
  491. })
  492. })
  493. describe('slots', () => {
  494. const E = defineCustomElement({
  495. render() {
  496. return [
  497. h('div', null, [
  498. renderSlot(this.$slots, 'default', undefined, () => [
  499. h('div', 'fallback'),
  500. ]),
  501. ]),
  502. h('div', null, renderSlot(this.$slots, 'named')),
  503. ]
  504. },
  505. })
  506. customElements.define('my-el-slots', E)
  507. test('render slots correctly', () => {
  508. container.innerHTML = `<my-el-slots><span>hi</span></my-el-slots>`
  509. const e = container.childNodes[0] as VueElement
  510. // native slots allocation does not affect innerHTML, so we just
  511. // verify that we've rendered the correct native slots here...
  512. expect(e.shadowRoot!.innerHTML).toBe(
  513. `<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`,
  514. )
  515. })
  516. })
  517. describe('provide/inject', () => {
  518. const Consumer = defineCustomElement({
  519. setup() {
  520. const foo = inject<Ref>('foo')!
  521. return () => h('div', foo.value)
  522. },
  523. })
  524. customElements.define('my-consumer', Consumer)
  525. test('over nested usage', async () => {
  526. const foo = ref('injected!')
  527. const Provider = defineCustomElement({
  528. provide: {
  529. foo,
  530. },
  531. render() {
  532. return h('my-consumer')
  533. },
  534. })
  535. customElements.define('my-provider', Provider)
  536. container.innerHTML = `<my-provider><my-provider>`
  537. const provider = container.childNodes[0] as VueElement
  538. const consumer = provider.shadowRoot!.childNodes[0] as VueElement
  539. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  540. foo.value = 'changed!'
  541. await nextTick()
  542. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  543. })
  544. test('over slot composition', async () => {
  545. const foo = ref('injected!')
  546. const Provider = defineCustomElement({
  547. provide: {
  548. foo,
  549. },
  550. render() {
  551. return renderSlot(this.$slots, 'default')
  552. },
  553. })
  554. customElements.define('my-provider-2', Provider)
  555. container.innerHTML = `<my-provider-2><my-consumer></my-consumer><my-provider-2>`
  556. const provider = container.childNodes[0]
  557. const consumer = provider.childNodes[0] as VueElement
  558. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  559. foo.value = 'changed!'
  560. await nextTick()
  561. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  562. })
  563. test('inherited from ancestors', async () => {
  564. const fooA = ref('FooA!')
  565. const fooB = ref('FooB!')
  566. const ProviderA = defineCustomElement({
  567. provide: {
  568. fooA,
  569. },
  570. render() {
  571. return h('provider-b')
  572. },
  573. })
  574. const ProviderB = defineCustomElement({
  575. provide: {
  576. fooB,
  577. },
  578. render() {
  579. return h('my-multi-consumer')
  580. },
  581. })
  582. const Consumer = defineCustomElement({
  583. setup() {
  584. const fooA = inject<Ref>('fooA')!
  585. const fooB = inject<Ref>('fooB')!
  586. return () => h('div', `${fooA.value} ${fooB.value}`)
  587. },
  588. })
  589. customElements.define('provider-a', ProviderA)
  590. customElements.define('provider-b', ProviderB)
  591. customElements.define('my-multi-consumer', Consumer)
  592. container.innerHTML = `<provider-a><provider-a>`
  593. const providerA = container.childNodes[0] as VueElement
  594. const providerB = providerA.shadowRoot!.childNodes[0] as VueElement
  595. const consumer = providerB.shadowRoot!.childNodes[0] as VueElement
  596. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>FooA! FooB!</div>`)
  597. fooA.value = 'changedA!'
  598. fooB.value = 'changedB!'
  599. await nextTick()
  600. expect(consumer.shadowRoot!.innerHTML).toBe(
  601. `<div>changedA! changedB!</div>`,
  602. )
  603. })
  604. })
  605. describe('styles', () => {
  606. function assertStyles(el: VueElement, css: string[]) {
  607. const styles = el.shadowRoot?.querySelectorAll('style')!
  608. expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
  609. for (let i = 0; i < css.length; i++) {
  610. expect(styles[i].textContent).toBe(css[i])
  611. }
  612. }
  613. test('should attach styles to shadow dom', async () => {
  614. const def = defineComponent({
  615. __hmrId: 'foo',
  616. styles: [`div { color: red; }`],
  617. render() {
  618. return h('div', 'hello')
  619. },
  620. })
  621. const Foo = defineCustomElement(def)
  622. customElements.define('my-el-with-styles', Foo)
  623. container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
  624. const el = container.childNodes[0] as VueElement
  625. const style = el.shadowRoot?.querySelector('style')!
  626. expect(style.textContent).toBe(`div { color: red; }`)
  627. // hmr
  628. __VUE_HMR_RUNTIME__.reload('foo', {
  629. ...def,
  630. styles: [`div { color: blue; }`, `div { color: yellow; }`],
  631. } as any)
  632. await nextTick()
  633. assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
  634. })
  635. test("child components should inject styles to root element's shadow root", async () => {
  636. const Baz = () => h(Bar)
  637. const Bar = defineComponent({
  638. __hmrId: 'bar',
  639. styles: [`div { color: green; }`, `div { color: blue; }`],
  640. render() {
  641. return 'bar'
  642. },
  643. })
  644. const Foo = defineCustomElement({
  645. styles: [`div { color: red; }`],
  646. render() {
  647. return [h(Baz), h(Baz)]
  648. },
  649. })
  650. customElements.define('my-el-with-child-styles', Foo)
  651. container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
  652. const el = container.childNodes[0] as VueElement
  653. // inject order should be child -> parent
  654. assertStyles(el, [
  655. `div { color: green; }`,
  656. `div { color: blue; }`,
  657. `div { color: red; }`,
  658. ])
  659. // hmr
  660. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  661. ...Bar,
  662. styles: [`div { color: red; }`, `div { color: yellow; }`],
  663. } as any)
  664. await nextTick()
  665. assertStyles(el, [
  666. `div { color: red; }`,
  667. `div { color: yellow; }`,
  668. `div { color: red; }`,
  669. ])
  670. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  671. ...Bar,
  672. styles: [`div { color: blue; }`],
  673. } as any)
  674. await nextTick()
  675. assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
  676. })
  677. test('with nonce', () => {
  678. const Foo = defineCustomElement(
  679. {
  680. styles: [`div { color: red; }`],
  681. render() {
  682. return h('div', 'hello')
  683. },
  684. },
  685. { nonce: 'xxx' },
  686. )
  687. customElements.define('my-el-with-nonce', Foo)
  688. container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
  689. const el = container.childNodes[0] as VueElement
  690. const style = el.shadowRoot?.querySelector('style')!
  691. expect(style.getAttribute('nonce')).toBe('xxx')
  692. })
  693. })
  694. describe('async', () => {
  695. test('should work', async () => {
  696. const loaderSpy = vi.fn()
  697. const E = defineCustomElement(
  698. defineAsyncComponent(() => {
  699. loaderSpy()
  700. return Promise.resolve({
  701. props: ['msg'],
  702. styles: [`div { color: red }`],
  703. render(this: any) {
  704. return h('div', null, this.msg)
  705. },
  706. })
  707. }),
  708. )
  709. customElements.define('my-el-async', E)
  710. container.innerHTML =
  711. `<my-el-async msg="hello"></my-el-async>` +
  712. `<my-el-async msg="world"></my-el-async>`
  713. await new Promise(r => setTimeout(r))
  714. // loader should be called only once
  715. expect(loaderSpy).toHaveBeenCalledTimes(1)
  716. const e1 = container.childNodes[0] as VueElement
  717. const e2 = container.childNodes[1] as VueElement
  718. // should inject styles
  719. expect(e1.shadowRoot!.innerHTML).toBe(
  720. `<style>div { color: red }</style><div>hello</div>`,
  721. )
  722. expect(e2.shadowRoot!.innerHTML).toBe(
  723. `<style>div { color: red }</style><div>world</div>`,
  724. )
  725. // attr
  726. e1.setAttribute('msg', 'attr')
  727. await nextTick()
  728. expect((e1 as any).msg).toBe('attr')
  729. expect(e1.shadowRoot!.innerHTML).toBe(
  730. `<style>div { color: red }</style><div>attr</div>`,
  731. )
  732. // props
  733. expect(`msg` in e1).toBe(true)
  734. ;(e1 as any).msg = 'prop'
  735. expect(e1.getAttribute('msg')).toBe('prop')
  736. expect(e1.shadowRoot!.innerHTML).toBe(
  737. `<style>div { color: red }</style><div>prop</div>`,
  738. )
  739. })
  740. test('set DOM property before resolve', async () => {
  741. const E = defineCustomElement(
  742. defineAsyncComponent(() => {
  743. return Promise.resolve({
  744. props: ['msg'],
  745. setup(props) {
  746. expect(typeof props.msg).toBe('string')
  747. },
  748. render(this: any) {
  749. return h('div', this.msg)
  750. },
  751. })
  752. }),
  753. )
  754. customElements.define('my-el-async-2', E)
  755. const e1 = new E()
  756. // set property before connect
  757. e1.msg = 'hello'
  758. const e2 = new E()
  759. container.appendChild(e1)
  760. container.appendChild(e2)
  761. // set property after connect but before resolve
  762. e2.msg = 'world'
  763. await new Promise(r => setTimeout(r))
  764. expect(e1.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  765. expect(e2.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  766. e1.msg = 'world'
  767. expect(e1.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  768. e2.msg = 'hello'
  769. expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  770. })
  771. test('Number prop casting before resolve', async () => {
  772. const E = defineCustomElement(
  773. defineAsyncComponent(() => {
  774. return Promise.resolve({
  775. props: { n: Number },
  776. setup(props) {
  777. expect(props.n).toBe(20)
  778. },
  779. render(this: any) {
  780. return h('div', this.n + ',' + typeof this.n)
  781. },
  782. })
  783. }),
  784. )
  785. customElements.define('my-el-async-3', E)
  786. container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`
  787. await new Promise(r => setTimeout(r))
  788. const e = container.childNodes[0] as VueElement
  789. expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
  790. })
  791. test('with slots', async () => {
  792. const E = defineCustomElement(
  793. defineAsyncComponent(() => {
  794. return Promise.resolve({
  795. render(this: any) {
  796. return [
  797. h('div', null, [
  798. renderSlot(this.$slots, 'default', undefined, () => [
  799. h('div', 'fallback'),
  800. ]),
  801. ]),
  802. h('div', null, renderSlot(this.$slots, 'named')),
  803. ]
  804. },
  805. })
  806. }),
  807. )
  808. customElements.define('my-el-async-slots', E)
  809. container.innerHTML = `<my-el-async-slots><span>hi</span></my-el-async-slots>`
  810. await new Promise(r => setTimeout(r))
  811. const e = container.childNodes[0] as VueElement
  812. expect(e.shadowRoot!.innerHTML).toBe(
  813. `<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`,
  814. )
  815. })
  816. })
  817. describe('shadowRoot: false', () => {
  818. const E = defineCustomElement({
  819. shadowRoot: false,
  820. props: {
  821. msg: {
  822. type: String,
  823. default: 'hello',
  824. },
  825. },
  826. render() {
  827. return h('div', this.msg)
  828. },
  829. })
  830. customElements.define('my-el-shadowroot-false', E)
  831. test('should work', async () => {
  832. function raf() {
  833. return new Promise(resolve => {
  834. requestAnimationFrame(resolve)
  835. })
  836. }
  837. container.innerHTML = `<my-el-shadowroot-false></my-el-shadowroot-false>`
  838. const e = container.childNodes[0] as VueElement
  839. await raf()
  840. expect(e).toBeInstanceOf(E)
  841. expect(e._instance).toBeTruthy()
  842. expect(e.innerHTML).toBe(`<div>hello</div>`)
  843. expect(e.shadowRoot).toBe(null)
  844. })
  845. const toggle = ref(true)
  846. const ES = defineCustomElement(
  847. {
  848. render() {
  849. return [
  850. renderSlot(this.$slots, 'default'),
  851. toggle.value ? renderSlot(this.$slots, 'named') : null,
  852. renderSlot(this.$slots, 'omitted', {}, () => [
  853. h('div', 'fallback'),
  854. ]),
  855. ]
  856. },
  857. },
  858. { shadowRoot: false },
  859. )
  860. customElements.define('my-el-shadowroot-false-slots', ES)
  861. test('should render slots', async () => {
  862. container.innerHTML =
  863. `<my-el-shadowroot-false-slots>` +
  864. `<span>default</span>text` +
  865. `<div slot="named">named</div>` +
  866. `</my-el-shadowroot-false-slots>`
  867. const e = container.childNodes[0] as VueElement
  868. // native slots allocation does not affect innerHTML, so we just
  869. // verify that we've rendered the correct native slots here...
  870. expect(e.innerHTML).toBe(
  871. `<span>default</span>text` +
  872. `<div slot="named">named</div>` +
  873. `<div>fallback</div>`,
  874. )
  875. toggle.value = false
  876. await nextTick()
  877. expect(e.innerHTML).toBe(
  878. `<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
  879. )
  880. })
  881. })
  882. describe('helpers', () => {
  883. test('useHost', () => {
  884. const Foo = defineCustomElement({
  885. setup() {
  886. const host = useHost()!
  887. host.setAttribute('id', 'host')
  888. return () => h('div', 'hello')
  889. },
  890. })
  891. customElements.define('my-el-use-host', Foo)
  892. container.innerHTML = `<my-el-use-host>`
  893. const el = container.childNodes[0] as VueElement
  894. expect(el.id).toBe('host')
  895. })
  896. test('useShadowRoot for style injection', () => {
  897. const Foo = defineCustomElement({
  898. setup() {
  899. const root = useShadowRoot()!
  900. const style = document.createElement('style')
  901. style.innerHTML = `div { color: red; }`
  902. root.appendChild(style)
  903. return () => h('div', 'hello')
  904. },
  905. })
  906. customElements.define('my-el-use-shadow-root', Foo)
  907. container.innerHTML = `<my-el-use-shadow-root>`
  908. const el = container.childNodes[0] as VueElement
  909. const style = el.shadowRoot?.querySelector('style')!
  910. expect(style.textContent).toBe(`div { color: red; }`)
  911. })
  912. })
  913. describe('expose', () => {
  914. test('expose attributes and callback', async () => {
  915. type SetValue = (value: string) => void
  916. let fn: MockedFunction<SetValue>
  917. const E = defineCustomElement({
  918. setup(_, { expose }) {
  919. const value = ref('hello')
  920. const setValue = (fn = vi.fn((_value: string) => {
  921. value.value = _value
  922. }))
  923. expose({
  924. setValue,
  925. value,
  926. })
  927. return () => h('div', null, [value.value])
  928. },
  929. })
  930. customElements.define('my-el-expose', E)
  931. container.innerHTML = `<my-el-expose></my-el-expose>`
  932. const e = container.childNodes[0] as VueElement & {
  933. value: string
  934. setValue: MockedFunction<SetValue>
  935. }
  936. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  937. expect(e.value).toBe('hello')
  938. expect(e.setValue).toBe(fn!)
  939. e.setValue('world')
  940. expect(e.value).toBe('world')
  941. await nextTick()
  942. expect(e.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  943. })
  944. test('warning when exposing an existing property', () => {
  945. const E = defineCustomElement({
  946. props: {
  947. value: String,
  948. },
  949. setup(props, { expose }) {
  950. expose({
  951. value: 'hello',
  952. })
  953. return () => h('div', null, [props.value])
  954. },
  955. })
  956. customElements.define('my-el-expose-two', E)
  957. container.innerHTML = `<my-el-expose-two value="world"></my-el-expose-two>`
  958. expect(
  959. `[Vue warn]: Exposed property "value" already exists on custom element.`,
  960. ).toHaveBeenWarned()
  961. })
  962. })
  963. test('async & nested custom elements', async () => {
  964. let fooVal: string | undefined = ''
  965. const E = defineCustomElement(
  966. defineAsyncComponent(() => {
  967. return Promise.resolve({
  968. setup(props) {
  969. provide('foo', 'foo')
  970. },
  971. render(this: any) {
  972. return h('div', null, [renderSlot(this.$slots, 'default')])
  973. },
  974. })
  975. }),
  976. )
  977. const EChild = defineCustomElement({
  978. setup(props) {
  979. fooVal = inject('foo')
  980. },
  981. render(this: any) {
  982. return h('div', null, 'child')
  983. },
  984. })
  985. customElements.define('my-el-async-nested-ce', E)
  986. customElements.define('slotted-child', EChild)
  987. container.innerHTML = `<my-el-async-nested-ce><div><slotted-child></slotted-child></div></my-el-async-nested-ce>`
  988. await new Promise(r => setTimeout(r))
  989. const e = container.childNodes[0] as VueElement
  990. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
  991. expect(fooVal).toBe('foo')
  992. })
  993. test('async & multiple levels of nested custom elements', async () => {
  994. let fooVal: string | undefined = ''
  995. let barVal: string | undefined = ''
  996. const E = defineCustomElement(
  997. defineAsyncComponent(() => {
  998. return Promise.resolve({
  999. setup(props) {
  1000. provide('foo', 'foo')
  1001. },
  1002. render(this: any) {
  1003. return h('div', null, [renderSlot(this.$slots, 'default')])
  1004. },
  1005. })
  1006. }),
  1007. )
  1008. const EChild = defineCustomElement({
  1009. setup(props) {
  1010. provide('bar', 'bar')
  1011. },
  1012. render(this: any) {
  1013. return h('div', null, [renderSlot(this.$slots, 'default')])
  1014. },
  1015. })
  1016. const EChild2 = defineCustomElement({
  1017. setup(props) {
  1018. fooVal = inject('foo')
  1019. barVal = inject('bar')
  1020. },
  1021. render(this: any) {
  1022. return h('div', null, 'child')
  1023. },
  1024. })
  1025. customElements.define('my-el-async-nested-m-ce', E)
  1026. customElements.define('slotted-child-m', EChild)
  1027. customElements.define('slotted-child2-m', EChild2)
  1028. container.innerHTML =
  1029. `<my-el-async-nested-m-ce>` +
  1030. `<div><slotted-child-m>` +
  1031. `<slotted-child2-m></slotted-child2-m>` +
  1032. `</slotted-child-m></div>` +
  1033. `</my-el-async-nested-m-ce>`
  1034. await new Promise(r => setTimeout(r))
  1035. const e = container.childNodes[0] as VueElement
  1036. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
  1037. expect(fooVal).toBe('foo')
  1038. expect(barVal).toBe('bar')
  1039. })
  1040. describe('configureApp', () => {
  1041. test('should work', () => {
  1042. const E = defineCustomElement(
  1043. () => {
  1044. const msg = inject('msg')
  1045. return () => h('div', msg!)
  1046. },
  1047. {
  1048. configureApp(app) {
  1049. app.provide('msg', 'app-injected')
  1050. },
  1051. },
  1052. )
  1053. customElements.define('my-element-with-app', E)
  1054. container.innerHTML = `<my-element-with-app></my-element-with-app>`
  1055. const e = container.childNodes[0] as VueElement
  1056. expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
  1057. })
  1058. })
  1059. // #9885
  1060. test('avoid double mount when prop is set immediately after mount', () => {
  1061. customElements.define(
  1062. 'my-input-dupe',
  1063. defineCustomElement({
  1064. props: {
  1065. value: String,
  1066. },
  1067. render() {
  1068. return 'hello'
  1069. },
  1070. }),
  1071. )
  1072. const container = document.createElement('div')
  1073. document.body.appendChild(container)
  1074. createApp({
  1075. render() {
  1076. return h('div', [
  1077. h('my-input-dupe', {
  1078. onVnodeMounted(vnode) {
  1079. vnode.el!.value = 'fesfes'
  1080. },
  1081. }),
  1082. ])
  1083. },
  1084. }).mount(container)
  1085. expect(container.children[0].children[0].shadowRoot?.innerHTML).toBe(
  1086. 'hello',
  1087. )
  1088. })
  1089. // #11081
  1090. test('Props can be casted when mounting custom elements in component rendering functions', async () => {
  1091. const E = defineCustomElement(
  1092. defineAsyncComponent(() =>
  1093. Promise.resolve({
  1094. props: ['fooValue'],
  1095. setup(props) {
  1096. expect(props.fooValue).toBe('fooValue')
  1097. return () => h('div', props.fooValue)
  1098. },
  1099. }),
  1100. ),
  1101. )
  1102. customElements.define('my-el-async-4', E)
  1103. const R = defineComponent({
  1104. setup() {
  1105. const fooValue = ref('fooValue')
  1106. return () => {
  1107. return h('div', null, [
  1108. h('my-el-async-4', {
  1109. fooValue: fooValue.value,
  1110. }),
  1111. ])
  1112. }
  1113. },
  1114. })
  1115. const app = createApp(R)
  1116. app.mount(container)
  1117. await new Promise(r => setTimeout(r))
  1118. const e = container.querySelector('my-el-async-4') as VueElement
  1119. expect(e.shadowRoot!.innerHTML).toBe(`<div>fooValue</div>`)
  1120. app.unmount()
  1121. })
  1122. // #11276
  1123. test('delete prop on attr removal', async () => {
  1124. const E = defineCustomElement({
  1125. props: {
  1126. boo: {
  1127. type: Boolean,
  1128. },
  1129. },
  1130. render() {
  1131. return this.boo + ',' + typeof this.boo
  1132. },
  1133. })
  1134. customElements.define('el-attr-removal', E)
  1135. container.innerHTML = '<el-attr-removal boo>'
  1136. const e = container.childNodes[0] as VueElement
  1137. expect(e.shadowRoot!.innerHTML).toBe(`true,boolean`)
  1138. e.removeAttribute('boo')
  1139. await nextTick()
  1140. expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`)
  1141. })
  1142. })