customElement.spec.ts 35 KB

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