customElement.spec.ts 71 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426
  1. import type { MockedFunction } from 'vitest'
  2. import {
  3. type HMRRuntime,
  4. type Ref,
  5. Teleport,
  6. type VueElement,
  7. createApp,
  8. defineAsyncComponent,
  9. defineComponent,
  10. defineCustomElement,
  11. h,
  12. inject,
  13. nextTick,
  14. onMounted,
  15. provide,
  16. ref,
  17. render,
  18. renderSlot,
  19. useHost,
  20. useShadowRoot,
  21. } from '../src'
  22. declare var __VUE_HMR_RUNTIME__: HMRRuntime
  23. describe('defineCustomElement', () => {
  24. const container = document.createElement('div')
  25. document.body.appendChild(container)
  26. beforeEach(() => {
  27. container.innerHTML = ''
  28. })
  29. describe('mounting/unmount', () => {
  30. const E = defineCustomElement({
  31. props: {
  32. msg: {
  33. type: String,
  34. default: 'hello',
  35. },
  36. },
  37. render() {
  38. return h('div', this.msg)
  39. },
  40. })
  41. customElements.define('my-element', E)
  42. test('should work', () => {
  43. container.innerHTML = `<my-element></my-element>`
  44. const e = container.childNodes[0] as VueElement
  45. expect(e).toBeInstanceOf(E)
  46. expect(e._instance).toBeTruthy()
  47. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  48. })
  49. test('should work w/ manual instantiation', () => {
  50. const e = new E({ msg: 'inline' })
  51. // should lazy init
  52. expect(e._instance).toBe(null)
  53. // should initialize on connect
  54. container.appendChild(e)
  55. expect(e._instance).toBeTruthy()
  56. expect(e.shadowRoot!.innerHTML).toBe(`<div>inline</div>`)
  57. })
  58. test('should unmount on remove', async () => {
  59. container.innerHTML = `<my-element></my-element>`
  60. const e = container.childNodes[0] as VueElement
  61. container.removeChild(e)
  62. await nextTick()
  63. expect(e._instance).toBe(null)
  64. expect(e.shadowRoot!.innerHTML).toBe('')
  65. })
  66. // #10610
  67. test('When elements move, avoid prematurely disconnecting MutationObserver', async () => {
  68. const CustomInput = defineCustomElement({
  69. props: ['value'],
  70. emits: ['update'],
  71. setup(props, { emit }) {
  72. return () =>
  73. h('input', {
  74. type: 'number',
  75. value: props.value,
  76. onInput: (e: InputEvent) => {
  77. const num = (e.target! as HTMLInputElement).valueAsNumber
  78. emit('update', Number.isNaN(num) ? null : num)
  79. },
  80. })
  81. },
  82. })
  83. customElements.define('my-el-input', CustomInput)
  84. const num = ref('12')
  85. const containerComp = defineComponent({
  86. setup() {
  87. return () => {
  88. return h('div', [
  89. h('my-el-input', {
  90. value: num.value,
  91. onUpdate: ($event: CustomEvent) => {
  92. num.value = $event.detail[0]
  93. },
  94. }),
  95. h('div', { id: 'move' }),
  96. ])
  97. }
  98. },
  99. })
  100. const app = createApp(containerComp)
  101. const container = document.createElement('div')
  102. document.body.appendChild(container)
  103. app.mount(container)
  104. const myInputEl = container.querySelector('my-el-input')!
  105. const inputEl = myInputEl.shadowRoot!.querySelector('input')!
  106. await nextTick()
  107. expect(inputEl.value).toBe('12')
  108. const moveEl = container.querySelector('#move')!
  109. moveEl.append(myInputEl)
  110. await nextTick()
  111. myInputEl.removeAttribute('value')
  112. await nextTick()
  113. expect(inputEl.value).toBe('')
  114. })
  115. test('should not unmount on move', async () => {
  116. container.innerHTML = `<div><my-element></my-element></div>`
  117. const e = container.childNodes[0].childNodes[0] as VueElement
  118. const i = e._instance
  119. // moving from one parent to another - this will trigger both disconnect
  120. // and connected callbacks synchronously
  121. container.appendChild(e)
  122. await nextTick()
  123. // should be the same instance
  124. expect(e._instance).toBe(i)
  125. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  126. })
  127. test('remove then insert again', async () => {
  128. container.innerHTML = `<my-element></my-element>`
  129. const e = container.childNodes[0] as VueElement
  130. container.removeChild(e)
  131. await nextTick()
  132. expect(e._instance).toBe(null)
  133. expect(e.shadowRoot!.innerHTML).toBe('')
  134. container.appendChild(e)
  135. expect(e._instance).toBeTruthy()
  136. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  137. })
  138. })
  139. describe('props', () => {
  140. const E = defineCustomElement({
  141. props: {
  142. foo: [String, null],
  143. bar: Object,
  144. bazQux: null,
  145. value: null,
  146. },
  147. render() {
  148. return [
  149. h('div', null, this.foo || ''),
  150. h('div', null, this.bazQux || (this.bar && this.bar.x)),
  151. ]
  152. },
  153. })
  154. customElements.define('my-el-props', E)
  155. test('renders custom element w/ correct object prop value', () => {
  156. render(h('my-el-props', { value: { x: 1 } }), container)
  157. const el = container.children[0]
  158. expect((el as any).value).toEqual({ x: 1 })
  159. })
  160. test('props via attribute', async () => {
  161. // bazQux should map to `baz-qux` attribute
  162. container.innerHTML = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
  163. const e = container.childNodes[0] as VueElement
  164. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
  165. // change attr
  166. e.setAttribute('foo', 'changed')
  167. await nextTick()
  168. expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div><div>bye</div>')
  169. e.setAttribute('baz-qux', 'changed')
  170. await nextTick()
  171. expect(e.shadowRoot!.innerHTML).toBe(
  172. '<div>changed</div><div>changed</div>',
  173. )
  174. })
  175. test('props via properties', async () => {
  176. const e = new E()
  177. e.foo = 'one'
  178. e.bar = { x: 'two' }
  179. container.appendChild(e)
  180. expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
  181. // reflect
  182. // should reflect primitive value
  183. expect(e.getAttribute('foo')).toBe('one')
  184. // should not reflect rich data
  185. expect(e.hasAttribute('bar')).toBe(false)
  186. e.foo = 'three'
  187. await nextTick()
  188. expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
  189. expect(e.getAttribute('foo')).toBe('three')
  190. e.foo = null
  191. await nextTick()
  192. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
  193. expect(e.hasAttribute('foo')).toBe(false)
  194. e.foo = undefined
  195. await nextTick()
  196. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
  197. expect(e.hasAttribute('foo')).toBe(false)
  198. expect(e.foo).toBe(undefined)
  199. e.bazQux = 'four'
  200. await nextTick()
  201. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
  202. expect(e.getAttribute('baz-qux')).toBe('four')
  203. })
  204. test('props via attributes and properties changed together', async () => {
  205. const e = new E()
  206. e.foo = 'foo1'
  207. e.bar = { x: 'bar1' }
  208. container.appendChild(e)
  209. await nextTick()
  210. expect(e.shadowRoot!.innerHTML).toBe('<div>foo1</div><div>bar1</div>')
  211. // change attr then property
  212. e.setAttribute('foo', 'foo2')
  213. e.bar = { x: 'bar2' }
  214. await nextTick()
  215. expect(e.shadowRoot!.innerHTML).toBe('<div>foo2</div><div>bar2</div>')
  216. expect(e.getAttribute('foo')).toBe('foo2')
  217. expect(e.hasAttribute('bar')).toBe(false)
  218. // change prop then attr
  219. e.bar = { x: 'bar3' }
  220. e.setAttribute('foo', 'foo3')
  221. await nextTick()
  222. expect(e.shadowRoot!.innerHTML).toBe('<div>foo3</div><div>bar3</div>')
  223. expect(e.getAttribute('foo')).toBe('foo3')
  224. expect(e.hasAttribute('bar')).toBe(false)
  225. })
  226. test('props via hyphen property', async () => {
  227. const Comp = defineCustomElement({
  228. props: {
  229. fooBar: Boolean,
  230. },
  231. render() {
  232. return 'Comp'
  233. },
  234. })
  235. customElements.define('my-el-comp', Comp)
  236. render(h('my-el-comp', { 'foo-bar': true }), container)
  237. const el = container.children[0]
  238. expect((el as any).outerHTML).toBe('<my-el-comp foo-bar=""></my-el-comp>')
  239. })
  240. test('attribute -> prop type casting', async () => {
  241. const E = defineCustomElement({
  242. props: {
  243. fooBar: Number, // test casting of camelCase prop names
  244. bar: Boolean,
  245. baz: String,
  246. },
  247. render() {
  248. return [
  249. this.fooBar,
  250. typeof this.fooBar,
  251. this.bar,
  252. typeof this.bar,
  253. this.baz,
  254. typeof this.baz,
  255. ].join(' ')
  256. },
  257. })
  258. customElements.define('my-el-props-cast', E)
  259. container.innerHTML = `<my-el-props-cast foo-bar="1" baz="12345"></my-el-props-cast>`
  260. const e = container.childNodes[0] as VueElement
  261. expect(e.shadowRoot!.innerHTML).toBe(
  262. `1 number false boolean 12345 string`,
  263. )
  264. e.setAttribute('bar', '')
  265. await nextTick()
  266. expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
  267. e.setAttribute('foo-bar', '2e1')
  268. await nextTick()
  269. expect(e.shadowRoot!.innerHTML).toBe(
  270. `20 number true boolean 12345 string`,
  271. )
  272. e.setAttribute('baz', '2e1')
  273. await nextTick()
  274. expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
  275. })
  276. // #4772
  277. test('attr casting w/ programmatic creation', () => {
  278. const E = defineCustomElement({
  279. props: {
  280. foo: Number,
  281. },
  282. render() {
  283. return `foo type: ${typeof this.foo}`
  284. },
  285. })
  286. customElements.define('my-element-programmatic', E)
  287. const el = document.createElement('my-element-programmatic') as any
  288. el.setAttribute('foo', '123')
  289. container.appendChild(el)
  290. expect(el.shadowRoot.innerHTML).toBe(`foo type: number`)
  291. })
  292. test('handling properties set before upgrading', () => {
  293. const E = defineCustomElement({
  294. props: {
  295. foo: String,
  296. dataAge: Number,
  297. },
  298. setup(props) {
  299. expect(props.foo).toBe('hello')
  300. expect(props.dataAge).toBe(5)
  301. },
  302. render() {
  303. return h('div', `foo: ${this.foo}`)
  304. },
  305. })
  306. const el = document.createElement('my-el-upgrade') as any
  307. el.foo = 'hello'
  308. el.dataset.age = 5
  309. el.notProp = 1
  310. container.appendChild(el)
  311. customElements.define('my-el-upgrade', E)
  312. expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`)
  313. // should not reflect if not declared as a prop
  314. expect(el.hasAttribute('not-prop')).toBe(false)
  315. })
  316. test('handle properties set before connecting', () => {
  317. const obj = { a: 1 }
  318. const E = defineCustomElement({
  319. props: {
  320. foo: String,
  321. post: Object,
  322. },
  323. setup(props) {
  324. expect(props.foo).toBe('hello')
  325. expect(props.post).toBe(obj)
  326. },
  327. render() {
  328. return JSON.stringify(this.post)
  329. },
  330. })
  331. customElements.define('my-el-preconnect', E)
  332. const el = document.createElement('my-el-preconnect') as any
  333. el.foo = 'hello'
  334. el.post = obj
  335. container.appendChild(el)
  336. expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj))
  337. })
  338. // https://github.com/vuejs/core/issues/6163
  339. test('handle components with no props', async () => {
  340. const E = defineCustomElement({
  341. render() {
  342. return h('div', 'foo')
  343. },
  344. })
  345. customElements.define('my-element-noprops', E)
  346. const el = document.createElement('my-element-noprops')
  347. container.appendChild(el)
  348. await nextTick()
  349. expect(el.shadowRoot!.innerHTML).toMatchInlineSnapshot('"<div>foo</div>"')
  350. })
  351. // #5793
  352. test('set number value in dom property', () => {
  353. const E = defineCustomElement({
  354. props: {
  355. 'max-age': Number,
  356. },
  357. render() {
  358. // @ts-expect-error
  359. return `max age: ${this.maxAge}/type: ${typeof this.maxAge}`
  360. },
  361. })
  362. customElements.define('my-element-number-property', E)
  363. const el = document.createElement('my-element-number-property') as any
  364. container.appendChild(el)
  365. el.maxAge = 50
  366. expect(el.maxAge).toBe(50)
  367. expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number')
  368. })
  369. // #9006
  370. test('should reflect default value', () => {
  371. const E = defineCustomElement({
  372. props: {
  373. value: {
  374. type: String,
  375. default: 'hi',
  376. },
  377. },
  378. render() {
  379. return this.value
  380. },
  381. })
  382. customElements.define('my-el-default-val', E)
  383. container.innerHTML = `<my-el-default-val></my-el-default-val>`
  384. const e = container.childNodes[0] as any
  385. expect(e.value).toBe('hi')
  386. })
  387. // #12214
  388. test('Boolean prop with default true', async () => {
  389. const E = defineCustomElement({
  390. props: {
  391. foo: {
  392. type: Boolean,
  393. default: true,
  394. },
  395. },
  396. render() {
  397. return String(this.foo)
  398. },
  399. })
  400. customElements.define('my-el-default-true', E)
  401. container.innerHTML = `<my-el-default-true></my-el-default-true>`
  402. const e = container.childNodes[0] as HTMLElement & { foo: any },
  403. shadowRoot = e.shadowRoot as ShadowRoot
  404. expect(shadowRoot.innerHTML).toBe('true')
  405. e.foo = undefined
  406. await nextTick()
  407. expect(shadowRoot.innerHTML).toBe('true')
  408. e.foo = false
  409. await nextTick()
  410. expect(shadowRoot.innerHTML).toBe('false')
  411. e.foo = null
  412. await nextTick()
  413. expect(shadowRoot.innerHTML).toBe('null')
  414. e.foo = ''
  415. await nextTick()
  416. expect(shadowRoot.innerHTML).toBe('true')
  417. })
  418. test('support direct setup function syntax with extra options', () => {
  419. const E = defineCustomElement(
  420. props => {
  421. return () => props.text
  422. },
  423. {
  424. props: {
  425. text: String,
  426. },
  427. },
  428. )
  429. customElements.define('my-el-setup-with-props', E)
  430. container.innerHTML = `<my-el-setup-with-props text="hello"></my-el-setup-with-props>`
  431. const e = container.childNodes[0] as VueElement
  432. expect(e.shadowRoot!.innerHTML).toBe('hello')
  433. })
  434. test('prop types validation', async () => {
  435. const E = defineCustomElement({
  436. props: {
  437. num: {
  438. type: [Number, String],
  439. },
  440. bool: {
  441. type: Boolean,
  442. },
  443. },
  444. render() {
  445. return h('div', [
  446. h('span', [`${this.num} is ${typeof this.num}`]),
  447. h('span', [`${this.bool} is ${typeof this.bool}`]),
  448. ])
  449. },
  450. })
  451. customElements.define('my-el-with-type-props', E)
  452. render(h('my-el-with-type-props', { num: 1, bool: true }), container)
  453. const e = container.childNodes[0] as VueElement
  454. // @ts-expect-error
  455. expect(e.num).toBe(1)
  456. // @ts-expect-error
  457. expect(e.bool).toBe(true)
  458. expect(e.shadowRoot!.innerHTML).toBe(
  459. '<div><span>1 is number</span><span>true is boolean</span></div>',
  460. )
  461. })
  462. test('should patch all props together', async () => {
  463. let prop1Calls = 0
  464. let prop2Calls = 0
  465. const E = defineCustomElement({
  466. props: {
  467. prop1: {
  468. type: String,
  469. default: 'default1',
  470. },
  471. prop2: {
  472. type: String,
  473. default: 'default2',
  474. },
  475. },
  476. data() {
  477. return {
  478. data1: 'defaultData1',
  479. data2: 'defaultData2',
  480. }
  481. },
  482. watch: {
  483. prop1(_) {
  484. prop1Calls++
  485. this.data2 = this.prop2
  486. },
  487. prop2(_) {
  488. prop2Calls++
  489. this.data1 = this.prop1
  490. },
  491. },
  492. render() {
  493. return h('div', [
  494. h('h1', this.prop1),
  495. h('h1', this.prop2),
  496. h('h2', this.data1),
  497. h('h2', this.data2),
  498. ])
  499. },
  500. })
  501. customElements.define('my-watch-element', E)
  502. render(h('my-watch-element'), container)
  503. const e = container.childNodes[0] as VueElement
  504. expect(e).toBeInstanceOf(E)
  505. expect(e._instance).toBeTruthy()
  506. expect(e.shadowRoot!.innerHTML).toBe(
  507. `<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
  508. )
  509. expect(prop1Calls).toBe(0)
  510. expect(prop2Calls).toBe(0)
  511. // patch props
  512. render(
  513. h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
  514. container,
  515. )
  516. await nextTick()
  517. expect(e.shadowRoot!.innerHTML).toBe(
  518. `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  519. )
  520. expect(prop1Calls).toBe(1)
  521. expect(prop2Calls).toBe(1)
  522. // same prop values
  523. render(
  524. h('my-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
  525. container,
  526. )
  527. await nextTick()
  528. expect(e.shadowRoot!.innerHTML).toBe(
  529. `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  530. )
  531. expect(prop1Calls).toBe(1)
  532. expect(prop2Calls).toBe(1)
  533. // update only prop1
  534. render(
  535. h('my-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
  536. container,
  537. )
  538. await nextTick()
  539. expect(e.shadowRoot!.innerHTML).toBe(
  540. `<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  541. )
  542. expect(prop1Calls).toBe(2)
  543. expect(prop2Calls).toBe(1)
  544. })
  545. test('should patch all props together (async)', async () => {
  546. let prop1Calls = 0
  547. let prop2Calls = 0
  548. const E = defineCustomElement(
  549. defineAsyncComponent(() =>
  550. Promise.resolve(
  551. defineComponent({
  552. props: {
  553. prop1: {
  554. type: String,
  555. default: 'default1',
  556. },
  557. prop2: {
  558. type: String,
  559. default: 'default2',
  560. },
  561. },
  562. data() {
  563. return {
  564. data1: 'defaultData1',
  565. data2: 'defaultData2',
  566. }
  567. },
  568. watch: {
  569. prop1(_) {
  570. prop1Calls++
  571. this.data2 = this.prop2
  572. },
  573. prop2(_) {
  574. prop2Calls++
  575. this.data1 = this.prop1
  576. },
  577. },
  578. render() {
  579. return h('div', [
  580. h('h1', this.prop1),
  581. h('h1', this.prop2),
  582. h('h2', this.data1),
  583. h('h2', this.data2),
  584. ])
  585. },
  586. }),
  587. ),
  588. ),
  589. )
  590. customElements.define('my-async-watch-element', E)
  591. render(h('my-async-watch-element'), container)
  592. await new Promise(r => setTimeout(r))
  593. const e = container.childNodes[0] as VueElement
  594. expect(e).toBeInstanceOf(E)
  595. expect(e._instance).toBeTruthy()
  596. expect(e.shadowRoot!.innerHTML).toBe(
  597. `<div><h1>default1</h1><h1>default2</h1><h2>defaultData1</h2><h2>defaultData2</h2></div>`,
  598. )
  599. expect(prop1Calls).toBe(0)
  600. expect(prop2Calls).toBe(0)
  601. // patch props
  602. render(
  603. h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
  604. container,
  605. )
  606. await nextTick()
  607. expect(e.shadowRoot!.innerHTML).toBe(
  608. `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  609. )
  610. expect(prop1Calls).toBe(1)
  611. expect(prop2Calls).toBe(1)
  612. // same prop values
  613. render(
  614. h('my-async-watch-element', { prop1: 'newValue1', prop2: 'newValue2' }),
  615. container,
  616. )
  617. await nextTick()
  618. expect(e.shadowRoot!.innerHTML).toBe(
  619. `<div><h1>newValue1</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  620. )
  621. expect(prop1Calls).toBe(1)
  622. expect(prop2Calls).toBe(1)
  623. // update only prop1
  624. render(
  625. h('my-async-watch-element', { prop1: 'newValue3', prop2: 'newValue2' }),
  626. container,
  627. )
  628. await nextTick()
  629. expect(e.shadowRoot!.innerHTML).toBe(
  630. `<div><h1>newValue3</h1><h1>newValue2</h1><h2>newValue1</h2><h2>newValue2</h2></div>`,
  631. )
  632. expect(prop1Calls).toBe(2)
  633. expect(prop2Calls).toBe(1)
  634. })
  635. })
  636. describe('attrs', () => {
  637. const E = defineCustomElement({
  638. render() {
  639. return [h('div', null, this.$attrs.foo as string)]
  640. },
  641. })
  642. customElements.define('my-el-attrs', E)
  643. test('attrs via attribute', async () => {
  644. container.innerHTML = `<my-el-attrs foo="hello"></my-el-attrs>`
  645. const e = container.childNodes[0] as VueElement
  646. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  647. e.setAttribute('foo', 'changed')
  648. await nextTick()
  649. expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div>')
  650. })
  651. test('non-declared properties should not show up in $attrs', () => {
  652. const e = new E()
  653. // @ts-expect-error
  654. e.foo = '123'
  655. container.appendChild(e)
  656. expect(e.shadowRoot!.innerHTML).toBe('<div></div>')
  657. })
  658. // #12408
  659. test('should set number tabindex as attribute', () => {
  660. render(h('my-el-attrs', { tabindex: 1, 'data-test': true }), container)
  661. const el = container.children[0] as HTMLElement
  662. expect(el.getAttribute('tabindex')).toBe('1')
  663. expect(el.getAttribute('data-test')).toBe('true')
  664. })
  665. test('should keep undeclared native attrs as attrs', () => {
  666. const root = document.createElement('div')
  667. document.body.appendChild(root)
  668. render(h('my-el-attrs', { translate: 'no' }), root)
  669. const el = root.children[0] as HTMLElement
  670. expect(el.getAttribute('translate')).toBe('no')
  671. expect(el.translate).toBe(false)
  672. render(null, root)
  673. root.remove()
  674. })
  675. // https://github.com/vuejs/core/issues/12964
  676. // Disabled because of missing support for `delegatesFocus` in jsdom
  677. // https://github.com/jsdom/jsdom/issues/3418
  678. // oxlint-disable-next-line vitest/no-disabled-tests
  679. test.skip('shadowRoot should be initialized with delegatesFocus', () => {
  680. const E = defineCustomElement(
  681. {
  682. render() {
  683. return [h('input', { tabindex: 1 })]
  684. },
  685. },
  686. { shadowRootOptions: { delegatesFocus: true } },
  687. )
  688. customElements.define('my-el-with-delegate-focus', E)
  689. const e = new E()
  690. container.appendChild(e)
  691. expect(e.shadowRoot!.delegatesFocus).toBe(true)
  692. })
  693. })
  694. describe('emits', () => {
  695. const CompDef = defineComponent({
  696. setup(_, { emit }) {
  697. emit('created')
  698. return () =>
  699. h('div', {
  700. onClick: () => {
  701. emit('my-click', 1)
  702. },
  703. onMousedown: () => {
  704. emit('myEvent', 1) // validate hyphenation
  705. },
  706. onWheel: () => {
  707. emit('my-wheel', { bubbles: true }, 1)
  708. },
  709. })
  710. },
  711. })
  712. const E = defineCustomElement(CompDef)
  713. customElements.define('my-el-emits', E)
  714. test('emit on connect', () => {
  715. const e = new E()
  716. const spy = vi.fn()
  717. e.addEventListener('created', spy)
  718. container.appendChild(e)
  719. expect(spy).toHaveBeenCalled()
  720. })
  721. test('emit on interaction', () => {
  722. container.innerHTML = `<my-el-emits></my-el-emits>`
  723. const e = container.childNodes[0] as VueElement
  724. const spy = vi.fn()
  725. e.addEventListener('my-click', spy)
  726. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  727. expect(spy).toHaveBeenCalledTimes(1)
  728. expect(spy.mock.calls[0][0]).toMatchObject({
  729. detail: [1],
  730. })
  731. })
  732. // #5373
  733. test('case transform for camelCase event', () => {
  734. container.innerHTML = `<my-el-emits></my-el-emits>`
  735. const e = container.childNodes[0] as VueElement
  736. const spy1 = vi.fn()
  737. e.addEventListener('myEvent', spy1)
  738. const spy2 = vi.fn()
  739. // emitting myEvent, but listening for my-event. This happens when
  740. // using the custom element in a Vue template
  741. e.addEventListener('my-event', spy2)
  742. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('mousedown'))
  743. expect(spy1).toHaveBeenCalledTimes(1)
  744. expect(spy2).toHaveBeenCalledTimes(1)
  745. })
  746. test('emit from within async component wrapper', async () => {
  747. const p = new Promise<typeof CompDef>(res => res(CompDef as any))
  748. const E = defineCustomElement(defineAsyncComponent(() => p))
  749. customElements.define('my-async-el-emits', E)
  750. container.innerHTML = `<my-async-el-emits></my-async-el-emits>`
  751. const e = container.childNodes[0] as VueElement
  752. const spy = vi.fn()
  753. e.addEventListener('my-click', spy)
  754. // this feels brittle but seems necessary to reach the node in the DOM.
  755. await customElements.whenDefined('my-async-el-emits')
  756. await nextTick()
  757. await nextTick()
  758. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  759. expect(spy).toHaveBeenCalled()
  760. expect(spy.mock.calls[0][0]).toMatchObject({
  761. detail: [1],
  762. })
  763. })
  764. // #7293
  765. test('emit in an async component wrapper with properties bound', async () => {
  766. const E = defineCustomElement(
  767. defineAsyncComponent(
  768. () => new Promise<typeof CompDef>(res => res(CompDef as any)),
  769. ),
  770. )
  771. customElements.define('my-async-el-props-emits', E)
  772. container.innerHTML = `<my-async-el-props-emits id="my_async_el_props_emits"></my-async-el-props-emits>`
  773. const e = container.childNodes[0] as VueElement
  774. const spy = vi.fn()
  775. e.addEventListener('my-click', spy)
  776. await customElements.whenDefined('my-async-el-props-emits')
  777. await nextTick()
  778. await nextTick()
  779. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  780. expect(spy).toHaveBeenCalled()
  781. expect(spy.mock.calls[0][0]).toMatchObject({
  782. detail: [1],
  783. })
  784. })
  785. test('emit with options', async () => {
  786. container.innerHTML = `<my-el-emits></my-el-emits>`
  787. const e = container.childNodes[0] as VueElement
  788. const spy = vi.fn()
  789. e.addEventListener('my-wheel', spy)
  790. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('wheel'))
  791. expect(spy).toHaveBeenCalledTimes(1)
  792. expect(spy.mock.calls[0][0]).toMatchObject({
  793. bubbles: true,
  794. detail: [{ bubbles: true }, 1],
  795. })
  796. })
  797. })
  798. describe('slots', () => {
  799. const E = defineCustomElement({
  800. render() {
  801. return [
  802. h('div', null, [
  803. renderSlot(this.$slots, 'default', undefined, () => [
  804. h('div', 'fallback'),
  805. ]),
  806. ]),
  807. h('div', null, renderSlot(this.$slots, 'named')),
  808. ]
  809. },
  810. })
  811. customElements.define('my-el-slots', E)
  812. test('render slots correctly', () => {
  813. container.innerHTML = `<my-el-slots><span>hi</span></my-el-slots>`
  814. const e = container.childNodes[0] as VueElement
  815. // native slots allocation does not affect innerHTML, so we just
  816. // verify that we've rendered the correct native slots here...
  817. expect(e.shadowRoot!.innerHTML).toBe(
  818. `<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`,
  819. )
  820. })
  821. test('render slot props', async () => {
  822. const foo = ref('foo')
  823. const E = defineCustomElement({
  824. render() {
  825. return [
  826. h(
  827. 'div',
  828. null,
  829. renderSlot(this.$slots, 'default', { class: foo.value }),
  830. ),
  831. ]
  832. },
  833. })
  834. customElements.define('my-el-slot-props', E)
  835. container.innerHTML = `<my-el-slot-props><span>hi</span></my-el-slot-props>`
  836. const e = container.childNodes[0] as VueElement
  837. expect(e.shadowRoot!.innerHTML).toBe(
  838. `<div><slot class="foo"></slot></div>`,
  839. )
  840. foo.value = 'bar'
  841. await nextTick()
  842. expect(e.shadowRoot!.innerHTML).toBe(
  843. `<div><slot class="bar"></slot></div>`,
  844. )
  845. })
  846. })
  847. describe('provide/inject', () => {
  848. const Consumer = defineCustomElement({
  849. setup() {
  850. const foo = inject<Ref>('foo')!
  851. return () => h('div', foo.value)
  852. },
  853. })
  854. customElements.define('my-consumer', Consumer)
  855. test('over nested usage', async () => {
  856. const foo = ref('injected!')
  857. const Provider = defineCustomElement({
  858. provide: {
  859. foo,
  860. },
  861. render() {
  862. return h('my-consumer')
  863. },
  864. })
  865. customElements.define('my-provider', Provider)
  866. container.innerHTML = `<my-provider><my-provider>`
  867. const provider = container.childNodes[0] as VueElement
  868. const consumer = provider.shadowRoot!.childNodes[0] as VueElement
  869. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  870. foo.value = 'changed!'
  871. await nextTick()
  872. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  873. })
  874. test('over slot composition', async () => {
  875. const foo = ref('injected!')
  876. const Provider = defineCustomElement({
  877. provide: {
  878. foo,
  879. },
  880. render() {
  881. return renderSlot(this.$slots, 'default')
  882. },
  883. })
  884. customElements.define('my-provider-2', Provider)
  885. container.innerHTML = `<my-provider-2><my-consumer></my-consumer><my-provider-2>`
  886. const provider = container.childNodes[0]
  887. const consumer = provider.childNodes[0] as VueElement
  888. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  889. foo.value = 'changed!'
  890. await nextTick()
  891. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  892. })
  893. test('inherited from ancestors', async () => {
  894. const fooA = ref('FooA!')
  895. const fooB = ref('FooB!')
  896. const ProviderA = defineCustomElement({
  897. provide: {
  898. fooA,
  899. },
  900. render() {
  901. return h('provider-b')
  902. },
  903. })
  904. const ProviderB = defineCustomElement({
  905. provide: {
  906. fooB,
  907. },
  908. render() {
  909. return h('my-multi-consumer')
  910. },
  911. })
  912. const Consumer = defineCustomElement({
  913. setup() {
  914. const fooA = inject<Ref>('fooA')!
  915. const fooB = inject<Ref>('fooB')!
  916. return () => h('div', `${fooA.value} ${fooB.value}`)
  917. },
  918. })
  919. customElements.define('provider-a', ProviderA)
  920. customElements.define('provider-b', ProviderB)
  921. customElements.define('my-multi-consumer', Consumer)
  922. container.innerHTML = `<provider-a><provider-a>`
  923. const providerA = container.childNodes[0] as VueElement
  924. const providerB = providerA.shadowRoot!.childNodes[0] as VueElement
  925. const consumer = providerB.shadowRoot!.childNodes[0] as VueElement
  926. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>FooA! FooB!</div>`)
  927. fooA.value = 'changedA!'
  928. fooB.value = 'changedB!'
  929. await nextTick()
  930. expect(consumer.shadowRoot!.innerHTML).toBe(
  931. `<div>changedA! changedB!</div>`,
  932. )
  933. })
  934. test('should resolve correct parent when element is slotted in shadow DOM', async () => {
  935. const GrandParent = defineCustomElement({
  936. provide: {
  937. foo: ref('GrandParent'),
  938. },
  939. render() {
  940. return h('my-parent-in-shadow', h('slot'))
  941. },
  942. })
  943. const Parent = defineCustomElement({
  944. provide: {
  945. foo: ref('Parent'),
  946. },
  947. render() {
  948. return h('slot')
  949. },
  950. })
  951. customElements.define('my-grand-parent', GrandParent)
  952. customElements.define('my-parent-in-shadow', Parent)
  953. container.innerHTML = `<my-grand-parent><my-consumer></my-consumer></my-grand-parent>`
  954. const grandParent = container.childNodes[0] as VueElement,
  955. consumer = grandParent.firstElementChild as VueElement
  956. expect(consumer.shadowRoot!.textContent).toBe('Parent')
  957. })
  958. // #13212
  959. test('inherited from app context within nested elements', async () => {
  960. const outerValues: (string | undefined)[] = []
  961. const innerValues: (string | undefined)[] = []
  962. const innerChildValues: (string | undefined)[] = []
  963. const Outer = defineCustomElement(
  964. {
  965. setup() {
  966. outerValues.push(
  967. inject<string>('shared'),
  968. inject<string>('outer'),
  969. inject<string>('inner'),
  970. )
  971. },
  972. render() {
  973. return h('div', [renderSlot(this.$slots, 'default')])
  974. },
  975. },
  976. {
  977. configureApp(app) {
  978. app.provide('shared', 'shared')
  979. app.provide('outer', 'outer')
  980. },
  981. },
  982. )
  983. const Inner = defineCustomElement(
  984. {
  985. setup() {
  986. // ensure values are not self-injected
  987. provide('inner', 'inner-child')
  988. innerValues.push(
  989. inject<string>('shared'),
  990. inject<string>('outer'),
  991. inject<string>('inner'),
  992. )
  993. },
  994. render() {
  995. return h('div', [renderSlot(this.$slots, 'default')])
  996. },
  997. },
  998. {
  999. configureApp(app) {
  1000. app.provide('outer', 'override-outer')
  1001. app.provide('inner', 'inner')
  1002. },
  1003. },
  1004. )
  1005. const InnerChild = defineCustomElement({
  1006. setup() {
  1007. innerChildValues.push(
  1008. inject<string>('shared'),
  1009. inject<string>('outer'),
  1010. inject<string>('inner'),
  1011. )
  1012. },
  1013. render() {
  1014. return h('div')
  1015. },
  1016. })
  1017. customElements.define('provide-from-app-outer', Outer)
  1018. customElements.define('provide-from-app-inner', Inner)
  1019. customElements.define('provide-from-app-inner-child', InnerChild)
  1020. container.innerHTML =
  1021. '<provide-from-app-outer>' +
  1022. '<provide-from-app-inner>' +
  1023. '<provide-from-app-inner-child></provide-from-app-inner-child>' +
  1024. '</provide-from-app-inner>' +
  1025. '</provide-from-app-outer>'
  1026. const outer = container.childNodes[0] as VueElement
  1027. expect(outer.shadowRoot!.innerHTML).toBe('<div><slot></slot></div>')
  1028. expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
  1029. 1,
  1030. )
  1031. expect(
  1032. '[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
  1033. 'It will be overwritten with the new value.',
  1034. ).toHaveBeenWarnedTimes(1)
  1035. expect(outerValues).toEqual(['shared', 'outer', undefined])
  1036. expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
  1037. expect(innerChildValues).toEqual([
  1038. 'shared',
  1039. 'override-outer',
  1040. 'inner-child',
  1041. ])
  1042. })
  1043. })
  1044. describe('styles', () => {
  1045. function assertStyles(el: VueElement, css: string[]) {
  1046. const styles = el.shadowRoot?.querySelectorAll('style')!
  1047. expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
  1048. for (let i = 0; i < css.length; i++) {
  1049. expect(styles[i].textContent).toBe(css[i])
  1050. }
  1051. }
  1052. test('should attach styles to shadow dom', async () => {
  1053. const def = defineComponent({
  1054. __hmrId: 'foo',
  1055. styles: [`div { color: red; }`],
  1056. render() {
  1057. return h('div', 'hello')
  1058. },
  1059. })
  1060. const Foo = defineCustomElement(def)
  1061. customElements.define('my-el-with-styles', Foo)
  1062. container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
  1063. const el = container.childNodes[0] as VueElement
  1064. const style = el.shadowRoot?.querySelector('style')!
  1065. expect(style.textContent).toBe(`div { color: red; }`)
  1066. // hmr
  1067. __VUE_HMR_RUNTIME__.reload('foo', {
  1068. ...def,
  1069. styles: [`div { color: blue; }`, `div { color: yellow; }`],
  1070. } as any)
  1071. await nextTick()
  1072. assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
  1073. })
  1074. test("child components should inject styles to root element's shadow root", async () => {
  1075. const Baz = () => h(Bar)
  1076. const Bar = defineComponent({
  1077. __hmrId: 'bar',
  1078. styles: [`div { color: green; }`, `div { color: blue; }`],
  1079. render() {
  1080. return 'bar'
  1081. },
  1082. })
  1083. const Foo = defineCustomElement({
  1084. styles: [`div { color: red; }`],
  1085. render() {
  1086. return [h(Baz), h(Baz)]
  1087. },
  1088. })
  1089. customElements.define('my-el-with-child-styles', Foo)
  1090. container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
  1091. const el = container.childNodes[0] as VueElement
  1092. // inject order should be child -> parent
  1093. assertStyles(el, [
  1094. `div { color: green; }`,
  1095. `div { color: blue; }`,
  1096. `div { color: red; }`,
  1097. ])
  1098. // hmr
  1099. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  1100. ...Bar,
  1101. styles: [`div { color: red; }`, `div { color: yellow; }`],
  1102. } as any)
  1103. await nextTick()
  1104. assertStyles(el, [
  1105. `div { color: red; }`,
  1106. `div { color: yellow; }`,
  1107. `div { color: red; }`,
  1108. ])
  1109. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  1110. ...Bar,
  1111. styles: [`div { color: blue; }`],
  1112. } as any)
  1113. await nextTick()
  1114. assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
  1115. })
  1116. test('root custom element HMR should preserve child-first style order', async () => {
  1117. const Child = defineComponent({
  1118. styles: [`div { color: green; }`],
  1119. render() {
  1120. return 'child'
  1121. },
  1122. })
  1123. const def = defineComponent({
  1124. __hmrId: 'root-child-style-order',
  1125. styles: [`div { color: red; }`],
  1126. render() {
  1127. return h(Child)
  1128. },
  1129. })
  1130. const Foo = defineCustomElement(def)
  1131. customElements.define('my-el-root-hmr-style-order', Foo)
  1132. container.innerHTML = `<my-el-root-hmr-style-order></my-el-root-hmr-style-order>`
  1133. const el = container.childNodes[0] as VueElement
  1134. assertStyles(el, [`div { color: green; }`, `div { color: red; }`])
  1135. __VUE_HMR_RUNTIME__.reload(def.__hmrId!, {
  1136. ...def,
  1137. styles: [`div { color: blue; }`, `div { color: yellow; }`],
  1138. } as any)
  1139. await nextTick()
  1140. assertStyles(el, [
  1141. `div { color: green; }`,
  1142. `div { color: blue; }`,
  1143. `div { color: yellow; }`,
  1144. ])
  1145. })
  1146. test('inject child component styles before parent styles', async () => {
  1147. const Baz = () => h(Bar)
  1148. const Bar = defineComponent({
  1149. styles: [`div { color: green; }`],
  1150. render() {
  1151. return 'bar'
  1152. },
  1153. })
  1154. const WrapperBar = defineComponent({
  1155. styles: [`div { color: blue; }`],
  1156. render() {
  1157. return h(Baz)
  1158. },
  1159. })
  1160. const WBaz = () => h(WrapperBar)
  1161. const Foo = defineCustomElement({
  1162. styles: [`div { color: red; }`],
  1163. render() {
  1164. return [h(Baz), h(WBaz)]
  1165. },
  1166. })
  1167. customElements.define('my-el-with-wrapper-child-styles', Foo)
  1168. container.innerHTML = `<my-el-with-wrapper-child-styles></my-el-with-wrapper-child-styles>`
  1169. const el = container.childNodes[0] as VueElement
  1170. // inject order should be child -> parent
  1171. assertStyles(el, [
  1172. `div { color: green; }`,
  1173. `div { color: blue; }`,
  1174. `div { color: red; }`,
  1175. ])
  1176. })
  1177. test('inject nested child component styles after HMR removes parent styles', async () => {
  1178. const Bar = defineComponent({
  1179. __hmrId: 'nested-child-style-hmr-bar',
  1180. styles: [`div { color: green; }`],
  1181. render() {
  1182. return 'bar'
  1183. },
  1184. })
  1185. const WrapperBar = defineComponent({
  1186. __hmrId: 'nested-child-style-hmr-wrapper',
  1187. styles: [`div { color: blue; }`],
  1188. render() {
  1189. return h(Bar)
  1190. },
  1191. })
  1192. const Foo = defineCustomElement({
  1193. styles: [`div { color: red; }`],
  1194. render() {
  1195. return h(WrapperBar)
  1196. },
  1197. })
  1198. customElements.define('my-el-with-hmr-nested-child-styles', Foo)
  1199. container.innerHTML = `<my-el-with-hmr-nested-child-styles></my-el-with-hmr-nested-child-styles>`
  1200. const el = container.childNodes[0] as VueElement
  1201. assertStyles(el, [
  1202. `div { color: green; }`,
  1203. `div { color: blue; }`,
  1204. `div { color: red; }`,
  1205. ])
  1206. __VUE_HMR_RUNTIME__.reload(WrapperBar.__hmrId!, {
  1207. ...WrapperBar,
  1208. styles: undefined,
  1209. } as any)
  1210. await nextTick()
  1211. assertStyles(el, [`div { color: green; }`, `div { color: red; }`])
  1212. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  1213. ...Bar,
  1214. styles: [`div { color: yellow; }`],
  1215. } as any)
  1216. await nextTick()
  1217. assertStyles(el, [`div { color: yellow; }`, `div { color: red; }`])
  1218. })
  1219. test('inject child component styles when parent has no styles', async () => {
  1220. const Baz = () => h(Bar)
  1221. const Bar = defineComponent({
  1222. styles: [`div { color: green; }`],
  1223. render() {
  1224. return 'bar'
  1225. },
  1226. })
  1227. const WrapperBar = defineComponent({
  1228. styles: [`div { color: blue; }`],
  1229. render() {
  1230. return h(Baz)
  1231. },
  1232. })
  1233. const WBaz = () => h(WrapperBar)
  1234. // without styles
  1235. const Foo = defineCustomElement({
  1236. render() {
  1237. return [h(Baz), h(WBaz)]
  1238. },
  1239. })
  1240. customElements.define('my-el-with-inject-child-styles', Foo)
  1241. container.innerHTML = `<my-el-with-inject-child-styles></my-el-with-inject-child-styles>`
  1242. const el = container.childNodes[0] as VueElement
  1243. assertStyles(el, [`div { color: green; }`, `div { color: blue; }`])
  1244. })
  1245. test('inject nested child component styles', async () => {
  1246. const Baz = defineComponent({
  1247. styles: [`div { color: yellow; }`],
  1248. render() {
  1249. return h(Bar)
  1250. },
  1251. })
  1252. const Bar = defineComponent({
  1253. styles: [`div { color: green; }`],
  1254. render() {
  1255. return 'bar'
  1256. },
  1257. })
  1258. const WrapperBar = defineComponent({
  1259. styles: [`div { color: blue; }`],
  1260. render() {
  1261. return h(Baz)
  1262. },
  1263. })
  1264. const WBaz = defineComponent({
  1265. styles: [`div { color: black; }`],
  1266. render() {
  1267. return h(WrapperBar)
  1268. },
  1269. })
  1270. const Foo = defineCustomElement({
  1271. styles: [`div { color: red; }`],
  1272. render() {
  1273. return [h(Baz), h(WBaz)]
  1274. },
  1275. })
  1276. customElements.define('my-el-with-inject-nested-child-styles', Foo)
  1277. container.innerHTML = `<my-el-with-inject-nested-child-styles></my-el-with-inject-nested-child-styles>`
  1278. const el = container.childNodes[0] as VueElement
  1279. assertStyles(el, [
  1280. `div { color: green; }`,
  1281. `div { color: yellow; }`,
  1282. `div { color: blue; }`,
  1283. `div { color: black; }`,
  1284. `div { color: red; }`,
  1285. ])
  1286. })
  1287. test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
  1288. const Bar = defineComponent({
  1289. styles: [`div { color: green; }`],
  1290. render() {
  1291. return 'bar'
  1292. },
  1293. })
  1294. const Baz = () => h(Bar)
  1295. const Foo = defineCustomElement(
  1296. {
  1297. render() {
  1298. return [h(Baz)]
  1299. },
  1300. },
  1301. { shadowRoot: false },
  1302. )
  1303. customElements.define('my-foo-with-shadowroot-false', Foo)
  1304. container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
  1305. const el = container.childNodes[0] as VueElement
  1306. const style = el.shadowRoot?.querySelector('style')
  1307. expect(style).toBeUndefined()
  1308. })
  1309. test('with nonce', () => {
  1310. const Foo = defineCustomElement(
  1311. {
  1312. styles: [`div { color: red; }`],
  1313. render() {
  1314. return h('div', 'hello')
  1315. },
  1316. },
  1317. { nonce: 'xxx' },
  1318. )
  1319. customElements.define('my-el-with-nonce', Foo)
  1320. container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
  1321. const el = container.childNodes[0] as VueElement
  1322. const style = el.shadowRoot?.querySelector('style')!
  1323. expect(style.getAttribute('nonce')).toBe('xxx')
  1324. })
  1325. })
  1326. describe('async', () => {
  1327. test('should work', async () => {
  1328. const loaderSpy = vi.fn()
  1329. const E = defineCustomElement(
  1330. defineAsyncComponent(() => {
  1331. loaderSpy()
  1332. return Promise.resolve({
  1333. props: ['msg'],
  1334. styles: [`div { color: red }`],
  1335. render(this: any) {
  1336. return h('div', null, this.msg)
  1337. },
  1338. })
  1339. }),
  1340. )
  1341. customElements.define('my-el-async', E)
  1342. container.innerHTML =
  1343. `<my-el-async msg="hello"></my-el-async>` +
  1344. `<my-el-async msg="world"></my-el-async>`
  1345. await new Promise(r => setTimeout(r))
  1346. // loader should be called only once
  1347. expect(loaderSpy).toHaveBeenCalledTimes(1)
  1348. const e1 = container.childNodes[0] as VueElement
  1349. const e2 = container.childNodes[1] as VueElement
  1350. // should inject styles
  1351. expect(e1.shadowRoot!.innerHTML).toBe(
  1352. `<style>div { color: red }</style><div>hello</div>`,
  1353. )
  1354. expect(e2.shadowRoot!.innerHTML).toBe(
  1355. `<style>div { color: red }</style><div>world</div>`,
  1356. )
  1357. // attr
  1358. e1.setAttribute('msg', 'attr')
  1359. await nextTick()
  1360. expect((e1 as any).msg).toBe('attr')
  1361. expect(e1.shadowRoot!.innerHTML).toBe(
  1362. `<style>div { color: red }</style><div>attr</div>`,
  1363. )
  1364. // props
  1365. expect(`msg` in e1).toBe(true)
  1366. ;(e1 as any).msg = 'prop'
  1367. expect(e1.getAttribute('msg')).toBe('prop')
  1368. expect(e1.shadowRoot!.innerHTML).toBe(
  1369. `<style>div { color: red }</style><div>prop</div>`,
  1370. )
  1371. })
  1372. test('set DOM property before resolve', async () => {
  1373. const E = defineCustomElement(
  1374. defineAsyncComponent(() => {
  1375. return Promise.resolve({
  1376. props: ['msg'],
  1377. setup(props) {
  1378. expect(typeof props.msg).toBe('string')
  1379. },
  1380. render(this: any) {
  1381. return h('div', this.msg)
  1382. },
  1383. })
  1384. }),
  1385. )
  1386. customElements.define('my-el-async-2', E)
  1387. const e1 = new E()
  1388. // set property before connect
  1389. e1.msg = 'hello'
  1390. const e2 = new E()
  1391. container.appendChild(e1)
  1392. container.appendChild(e2)
  1393. // set property after connect but before resolve
  1394. e2.msg = 'world'
  1395. await new Promise(r => setTimeout(r))
  1396. expect(e1.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1397. expect(e2.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1398. e1.msg = 'world'
  1399. expect(e1.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1400. e2.msg = 'hello'
  1401. expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1402. })
  1403. test('render object prop before resolve', async () => {
  1404. const AsyncComp = defineComponent({
  1405. props: { value: Object },
  1406. render(this: any) {
  1407. return h('div', this.value.x)
  1408. },
  1409. })
  1410. let resolve!: (comp: typeof AsyncComp) => void
  1411. const p = new Promise<typeof AsyncComp>(res => {
  1412. resolve = res
  1413. })
  1414. const E = defineCustomElement(defineAsyncComponent(() => p))
  1415. customElements.define('my-el-async-object-prop', E)
  1416. const root = document.createElement('div')
  1417. document.body.appendChild(root)
  1418. const value = { x: 1 }
  1419. render(h('my-el-async-object-prop', { value }), root)
  1420. const el = root.children[0] as VueElement & { value: typeof value }
  1421. expect(el.value).toBe(value)
  1422. expect(el.getAttribute('value')).toBe(null)
  1423. resolve(AsyncComp)
  1424. await new Promise(r => setTimeout(r))
  1425. expect(el.value).toBe(value)
  1426. expect(el.getAttribute('value')).toBe(null)
  1427. expect(el.shadowRoot!.innerHTML).toBe(`<div>1</div>`)
  1428. render(null, root)
  1429. root.remove()
  1430. })
  1431. test('Number prop casting before resolve', async () => {
  1432. const E = defineCustomElement(
  1433. defineAsyncComponent(() => {
  1434. return Promise.resolve({
  1435. props: { n: Number },
  1436. setup(props) {
  1437. expect(props.n).toBe(20)
  1438. },
  1439. render(this: any) {
  1440. return h('div', this.n + ',' + typeof this.n)
  1441. },
  1442. })
  1443. }),
  1444. )
  1445. customElements.define('my-el-async-3', E)
  1446. container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`
  1447. await new Promise(r => setTimeout(r))
  1448. const e = container.childNodes[0] as VueElement
  1449. expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
  1450. })
  1451. test('with slots', async () => {
  1452. const E = defineCustomElement(
  1453. defineAsyncComponent(() => {
  1454. return Promise.resolve({
  1455. render(this: any) {
  1456. return [
  1457. h('div', null, [
  1458. renderSlot(this.$slots, 'default', undefined, () => [
  1459. h('div', 'fallback'),
  1460. ]),
  1461. ]),
  1462. h('div', null, renderSlot(this.$slots, 'named')),
  1463. ]
  1464. },
  1465. })
  1466. }),
  1467. )
  1468. customElements.define('my-el-async-slots', E)
  1469. container.innerHTML = `<my-el-async-slots><span>hi</span></my-el-async-slots>`
  1470. await new Promise(r => setTimeout(r))
  1471. const e = container.childNodes[0] as VueElement
  1472. expect(e.shadowRoot!.innerHTML).toBe(
  1473. `<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`,
  1474. )
  1475. })
  1476. })
  1477. describe('shadowRoot: false', () => {
  1478. const E = defineCustomElement({
  1479. shadowRoot: false,
  1480. props: {
  1481. msg: {
  1482. type: String,
  1483. default: 'hello',
  1484. },
  1485. },
  1486. render() {
  1487. return h('div', this.msg)
  1488. },
  1489. })
  1490. customElements.define('my-el-shadowroot-false', E)
  1491. test('should work', async () => {
  1492. function raf() {
  1493. return new Promise(resolve => {
  1494. requestAnimationFrame(resolve)
  1495. })
  1496. }
  1497. container.innerHTML = `<my-el-shadowroot-false></my-el-shadowroot-false>`
  1498. const e = container.childNodes[0] as VueElement
  1499. await raf()
  1500. expect(e).toBeInstanceOf(E)
  1501. expect(e._instance).toBeTruthy()
  1502. expect(e.innerHTML).toBe(`<div>hello</div>`)
  1503. expect(e.shadowRoot).toBe(null)
  1504. })
  1505. const toggle = ref(true)
  1506. const ES = defineCustomElement(
  1507. {
  1508. render() {
  1509. return [
  1510. renderSlot(this.$slots, 'default'),
  1511. toggle.value ? renderSlot(this.$slots, 'named') : null,
  1512. renderSlot(this.$slots, 'omitted', {}, () => [
  1513. h('div', 'fallback'),
  1514. ]),
  1515. ]
  1516. },
  1517. },
  1518. { shadowRoot: false },
  1519. )
  1520. customElements.define('my-el-shadowroot-false-slots', ES)
  1521. test('should render slots', async () => {
  1522. container.innerHTML =
  1523. `<my-el-shadowroot-false-slots>` +
  1524. `<span>default</span>text` +
  1525. `<div slot="named">named</div>` +
  1526. `</my-el-shadowroot-false-slots>`
  1527. const e = container.childNodes[0] as VueElement
  1528. // native slots allocation does not affect innerHTML, so we just
  1529. // verify that we've rendered the correct native slots here...
  1530. expect(e.innerHTML).toBe(
  1531. `<span>default</span>text` +
  1532. `<div slot="named">named</div>` +
  1533. `<div>fallback</div>`,
  1534. )
  1535. toggle.value = false
  1536. await nextTick()
  1537. expect(e.innerHTML).toBe(
  1538. `<span>default</span>text` + `<!---->` + `<div>fallback</div>`,
  1539. )
  1540. })
  1541. test('render nested customElement w/ shadowRoot false', async () => {
  1542. const calls: string[] = []
  1543. const Child = defineCustomElement(
  1544. {
  1545. setup() {
  1546. calls.push('child rendering')
  1547. onMounted(() => {
  1548. calls.push('child mounted')
  1549. })
  1550. },
  1551. render() {
  1552. return renderSlot(this.$slots, 'default')
  1553. },
  1554. },
  1555. { shadowRoot: false },
  1556. )
  1557. customElements.define('my-child', Child)
  1558. const Parent = defineCustomElement(
  1559. {
  1560. setup() {
  1561. calls.push('parent rendering')
  1562. onMounted(() => {
  1563. calls.push('parent mounted')
  1564. })
  1565. },
  1566. render() {
  1567. return renderSlot(this.$slots, 'default')
  1568. },
  1569. },
  1570. { shadowRoot: false },
  1571. )
  1572. customElements.define('my-parent', Parent)
  1573. const App = {
  1574. render() {
  1575. return h('my-parent', null, {
  1576. default: () => [
  1577. h('my-child', null, {
  1578. default: () => [h('span', null, 'default')],
  1579. }),
  1580. ],
  1581. })
  1582. },
  1583. }
  1584. const app = createApp(App)
  1585. app.mount(container)
  1586. await nextTick()
  1587. const e = container.childNodes[0] as VueElement
  1588. expect(e.innerHTML).toBe(
  1589. `<my-child data-v-app=""><span>default</span></my-child>`,
  1590. )
  1591. expect(calls).toEqual([
  1592. 'parent rendering',
  1593. 'parent mounted',
  1594. 'child rendering',
  1595. 'child mounted',
  1596. ])
  1597. app.unmount()
  1598. })
  1599. test('render nested Teleport w/ shadowRoot false', async () => {
  1600. const target = document.createElement('div')
  1601. const Child = defineCustomElement(
  1602. {
  1603. render() {
  1604. return h(
  1605. Teleport,
  1606. { to: target },
  1607. {
  1608. default: () => [renderSlot(this.$slots, 'default')],
  1609. },
  1610. )
  1611. },
  1612. },
  1613. { shadowRoot: false },
  1614. )
  1615. customElements.define('my-el-teleport-child', Child)
  1616. const Parent = defineCustomElement(
  1617. {
  1618. render() {
  1619. return renderSlot(this.$slots, 'default')
  1620. },
  1621. },
  1622. { shadowRoot: false },
  1623. )
  1624. customElements.define('my-el-teleport-parent', Parent)
  1625. const App = {
  1626. render() {
  1627. return h('my-el-teleport-parent', null, {
  1628. default: () => [
  1629. h('my-el-teleport-child', null, {
  1630. default: () => [h('span', null, 'default')],
  1631. }),
  1632. ],
  1633. })
  1634. },
  1635. }
  1636. const app = createApp(App)
  1637. app.mount(container)
  1638. await nextTick()
  1639. expect(target.innerHTML).toBe(`<span>default</span>`)
  1640. app.unmount()
  1641. })
  1642. test('render two Teleports w/ shadowRoot false', async () => {
  1643. const target1 = document.createElement('div')
  1644. const target2 = document.createElement('span')
  1645. const Child = defineCustomElement(
  1646. {
  1647. render() {
  1648. return [
  1649. h(Teleport, { to: target1 }, [renderSlot(this.$slots, 'header')]),
  1650. h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]),
  1651. ]
  1652. },
  1653. },
  1654. { shadowRoot: false },
  1655. )
  1656. customElements.define('my-el-two-teleport-child', Child)
  1657. const App = {
  1658. render() {
  1659. return h('my-el-two-teleport-child', null, {
  1660. default: () => [
  1661. h('div', { slot: 'header' }, 'header'),
  1662. h('span', { slot: 'body' }, 'body'),
  1663. ],
  1664. })
  1665. },
  1666. }
  1667. const app = createApp(App)
  1668. app.mount(container)
  1669. await nextTick()
  1670. expect(target1.outerHTML).toBe(
  1671. `<div><div slot="header">header</div></div>`,
  1672. )
  1673. expect(target2.outerHTML).toBe(
  1674. `<span><span slot="body">body</span></span>`,
  1675. )
  1676. app.unmount()
  1677. })
  1678. test('render two Teleports w/ shadowRoot false (with disabled)', async () => {
  1679. const target1 = document.createElement('div')
  1680. const target2 = document.createElement('span')
  1681. const Child = defineCustomElement(
  1682. {
  1683. render() {
  1684. return [
  1685. // with disabled: true
  1686. h(Teleport, { to: target1, disabled: true }, [
  1687. renderSlot(this.$slots, 'header'),
  1688. ]),
  1689. h(Teleport, { to: target2 }, [renderSlot(this.$slots, 'body')]),
  1690. ]
  1691. },
  1692. },
  1693. { shadowRoot: false },
  1694. )
  1695. customElements.define('my-el-two-teleport-child-0', Child)
  1696. const App = {
  1697. render() {
  1698. return h('my-el-two-teleport-child-0', null, {
  1699. default: () => [
  1700. h('div', { slot: 'header' }, 'header'),
  1701. h('span', { slot: 'body' }, 'body'),
  1702. ],
  1703. })
  1704. },
  1705. }
  1706. const app = createApp(App)
  1707. app.mount(container)
  1708. await nextTick()
  1709. expect(target1.outerHTML).toBe(`<div></div>`)
  1710. expect(target2.outerHTML).toBe(
  1711. `<span><span slot="body">body</span></span>`,
  1712. )
  1713. app.unmount()
  1714. })
  1715. test('teleport target is ancestor of custom element host', async () => {
  1716. const Child = defineCustomElement(
  1717. {
  1718. render() {
  1719. return [
  1720. h(Teleport, { to: '#t1' }, [renderSlot(this.$slots, 'header')]),
  1721. ]
  1722. },
  1723. },
  1724. { shadowRoot: false },
  1725. )
  1726. customElements.define('my-el-teleport-child-target', Child)
  1727. const App = {
  1728. render() {
  1729. return h('div', { id: 't1' }, [
  1730. h('my-el-teleport-child-target', null, {
  1731. default: () => [h('div', { slot: 'header' }, 'header')],
  1732. }),
  1733. ])
  1734. },
  1735. }
  1736. const app = createApp(App)
  1737. app.mount(container)
  1738. const target1 = document.getElementById('t1')!
  1739. expect(target1.outerHTML).toBe(
  1740. `<div id="t1">` +
  1741. `<my-el-teleport-child-target data-v-app="">` +
  1742. `<!--teleport start--><!--teleport end-->` +
  1743. `</my-el-teleport-child-target>` +
  1744. `<div slot="header">header</div>` +
  1745. `</div>`,
  1746. )
  1747. app.unmount()
  1748. })
  1749. test('toggle nested custom element with shadowRoot: false', async () => {
  1750. customElements.define(
  1751. 'my-el-child-shadow-false',
  1752. defineCustomElement(
  1753. {
  1754. render(ctx: any) {
  1755. return h('div', null, [renderSlot(ctx.$slots, 'default')])
  1756. },
  1757. },
  1758. { shadowRoot: false },
  1759. ),
  1760. )
  1761. const ChildWrapper = {
  1762. render() {
  1763. return h('my-el-child-shadow-false', null, 'child')
  1764. },
  1765. }
  1766. customElements.define(
  1767. 'my-el-parent-shadow-false',
  1768. defineCustomElement(
  1769. {
  1770. props: {
  1771. isShown: { type: Boolean, required: true },
  1772. },
  1773. render(ctx: any, _: any, $props: any) {
  1774. return $props.isShown
  1775. ? h('div', { key: 0 }, [renderSlot(ctx.$slots, 'default')])
  1776. : null
  1777. },
  1778. },
  1779. { shadowRoot: false },
  1780. ),
  1781. )
  1782. const ParentWrapper = {
  1783. props: {
  1784. isShown: { type: Boolean, required: true },
  1785. },
  1786. render(ctx: any, _: any, $props: any) {
  1787. return h('my-el-parent-shadow-false', { isShown: $props.isShown }, [
  1788. renderSlot(ctx.$slots, 'default'),
  1789. ])
  1790. },
  1791. }
  1792. const isShown = ref(true)
  1793. const App = {
  1794. render() {
  1795. return h(ParentWrapper, { isShown: isShown.value } as any, {
  1796. default: () => [h(ChildWrapper)],
  1797. })
  1798. },
  1799. }
  1800. const container = document.createElement('div')
  1801. document.body.appendChild(container)
  1802. const app = createApp(App)
  1803. app.mount(container)
  1804. expect(container.innerHTML).toBe(
  1805. `<my-el-parent-shadow-false is-shown="" data-v-app="">` +
  1806. `<div>` +
  1807. `<my-el-child-shadow-false data-v-app="">` +
  1808. `<div>child</div>` +
  1809. `</my-el-child-shadow-false>` +
  1810. `</div>` +
  1811. `</my-el-parent-shadow-false>`,
  1812. )
  1813. isShown.value = false
  1814. await nextTick()
  1815. expect(container.innerHTML).toBe(
  1816. `<my-el-parent-shadow-false data-v-app=""><!----></my-el-parent-shadow-false>`,
  1817. )
  1818. isShown.value = true
  1819. await nextTick()
  1820. expect(container.innerHTML).toBe(
  1821. `<my-el-parent-shadow-false data-v-app="" is-shown="">` +
  1822. `<div>` +
  1823. `<my-el-child-shadow-false data-v-app="">` +
  1824. `<div>child</div>` +
  1825. `</my-el-child-shadow-false>` +
  1826. `</div>` +
  1827. `</my-el-parent-shadow-false>`,
  1828. )
  1829. })
  1830. })
  1831. describe('helpers', () => {
  1832. test('useHost', () => {
  1833. const Foo = defineCustomElement({
  1834. setup() {
  1835. const host = useHost()!
  1836. host.setAttribute('id', 'host')
  1837. return () => h('div', 'hello')
  1838. },
  1839. })
  1840. customElements.define('my-el-use-host', Foo)
  1841. container.innerHTML = `<my-el-use-host>`
  1842. const el = container.childNodes[0] as VueElement
  1843. expect(el.id).toBe('host')
  1844. })
  1845. test('useShadowRoot for style injection', () => {
  1846. const Foo = defineCustomElement({
  1847. setup() {
  1848. const root = useShadowRoot()!
  1849. const style = document.createElement('style')
  1850. style.innerHTML = `div { color: red; }`
  1851. root.appendChild(style)
  1852. return () => h('div', 'hello')
  1853. },
  1854. })
  1855. customElements.define('my-el-use-shadow-root', Foo)
  1856. container.innerHTML = `<my-el-use-shadow-root>`
  1857. const el = container.childNodes[0] as VueElement
  1858. const style = el.shadowRoot?.querySelector('style')!
  1859. expect(style.textContent).toBe(`div { color: red; }`)
  1860. })
  1861. })
  1862. describe('expose', () => {
  1863. test('expose w/ options api', async () => {
  1864. const E = defineCustomElement({
  1865. data() {
  1866. return {
  1867. value: 0,
  1868. }
  1869. },
  1870. methods: {
  1871. foo() {
  1872. ;(this as any).value++
  1873. },
  1874. },
  1875. expose: ['foo'],
  1876. render(_ctx: any) {
  1877. return h('div', null, _ctx.value)
  1878. },
  1879. })
  1880. customElements.define('my-el-expose-options-api', E)
  1881. container.innerHTML = `<my-el-expose-options-api></my-el-expose-options-api>`
  1882. const e = container.childNodes[0] as VueElement & {
  1883. foo: () => void
  1884. }
  1885. expect(e.shadowRoot!.innerHTML).toBe(`<div>0</div>`)
  1886. e.foo()
  1887. await nextTick()
  1888. expect(e.shadowRoot!.innerHTML).toBe(`<div>1</div>`)
  1889. })
  1890. test('expose attributes and callback', async () => {
  1891. type SetValue = (value: string) => void
  1892. let fn: MockedFunction<SetValue>
  1893. const E = defineCustomElement({
  1894. setup(_, { expose }) {
  1895. const value = ref('hello')
  1896. const setValue = (fn = vi.fn((_value: string) => {
  1897. value.value = _value
  1898. }))
  1899. expose({
  1900. setValue,
  1901. value,
  1902. })
  1903. return () => h('div', null, [value.value])
  1904. },
  1905. })
  1906. customElements.define('my-el-expose', E)
  1907. container.innerHTML = `<my-el-expose></my-el-expose>`
  1908. const e = container.childNodes[0] as VueElement & {
  1909. value: string
  1910. setValue: MockedFunction<SetValue>
  1911. }
  1912. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1913. expect(e.value).toBe('hello')
  1914. expect(e.setValue).toBe(fn!)
  1915. e.setValue('world')
  1916. expect(e.value).toBe('world')
  1917. await nextTick()
  1918. expect(e.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1919. })
  1920. test('warning when exposing an existing property', () => {
  1921. const E = defineCustomElement({
  1922. props: {
  1923. value: String,
  1924. },
  1925. setup(props, { expose }) {
  1926. expose({
  1927. value: 'hello',
  1928. })
  1929. return () => h('div', null, [props.value])
  1930. },
  1931. })
  1932. customElements.define('my-el-expose-two', E)
  1933. container.innerHTML = `<my-el-expose-two value="world"></my-el-expose-two>`
  1934. expect(
  1935. `[Vue warn]: Exposed property "value" already exists on custom element.`,
  1936. ).toHaveBeenWarned()
  1937. })
  1938. })
  1939. test('async & nested custom elements', async () => {
  1940. let fooVal: string | undefined = ''
  1941. const E = defineCustomElement(
  1942. defineAsyncComponent(() => {
  1943. return Promise.resolve({
  1944. setup(props) {
  1945. provide('foo', 'foo')
  1946. },
  1947. render(this: any) {
  1948. return h('div', null, [renderSlot(this.$slots, 'default')])
  1949. },
  1950. })
  1951. }),
  1952. )
  1953. const EChild = defineCustomElement({
  1954. setup(props) {
  1955. fooVal = inject('foo')
  1956. },
  1957. render(this: any) {
  1958. return h('div', null, 'child')
  1959. },
  1960. })
  1961. customElements.define('my-el-async-nested-ce', E)
  1962. customElements.define('slotted-child', EChild)
  1963. container.innerHTML = `<my-el-async-nested-ce><div><slotted-child></slotted-child></div></my-el-async-nested-ce>`
  1964. await new Promise(r => setTimeout(r))
  1965. const e = container.childNodes[0] as VueElement
  1966. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
  1967. expect(fooVal).toBe('foo')
  1968. })
  1969. test('async & multiple levels of nested custom elements', async () => {
  1970. let fooVal: string | undefined = ''
  1971. let barVal: string | undefined = ''
  1972. const E = defineCustomElement(
  1973. defineAsyncComponent(() => {
  1974. return Promise.resolve({
  1975. setup(props) {
  1976. provide('foo', 'foo')
  1977. },
  1978. render(this: any) {
  1979. return h('div', null, [renderSlot(this.$slots, 'default')])
  1980. },
  1981. })
  1982. }),
  1983. )
  1984. const EChild = defineCustomElement({
  1985. setup(props) {
  1986. provide('bar', 'bar')
  1987. },
  1988. render(this: any) {
  1989. return h('div', null, [renderSlot(this.$slots, 'default')])
  1990. },
  1991. })
  1992. const EChild2 = defineCustomElement({
  1993. setup(props) {
  1994. fooVal = inject('foo')
  1995. barVal = inject('bar')
  1996. },
  1997. render(this: any) {
  1998. return h('div', null, 'child')
  1999. },
  2000. })
  2001. customElements.define('my-el-async-nested-m-ce', E)
  2002. customElements.define('slotted-child-m', EChild)
  2003. customElements.define('slotted-child2-m', EChild2)
  2004. container.innerHTML =
  2005. `<my-el-async-nested-m-ce>` +
  2006. `<div><slotted-child-m>` +
  2007. `<slotted-child2-m></slotted-child2-m>` +
  2008. `</slotted-child-m></div>` +
  2009. `</my-el-async-nested-m-ce>`
  2010. await new Promise(r => setTimeout(r))
  2011. const e = container.childNodes[0] as VueElement
  2012. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot></div>`)
  2013. expect(fooVal).toBe('foo')
  2014. expect(barVal).toBe('bar')
  2015. })
  2016. describe('configureApp', () => {
  2017. test('should work', () => {
  2018. const E = defineCustomElement(
  2019. () => {
  2020. const msg = inject('msg')
  2021. return () => h('div', msg!)
  2022. },
  2023. {
  2024. configureApp(app) {
  2025. app.provide('msg', 'app-injected')
  2026. },
  2027. },
  2028. )
  2029. customElements.define('my-element-with-app', E)
  2030. container.innerHTML = `<my-element-with-app></my-element-with-app>`
  2031. const e = container.childNodes[0] as VueElement
  2032. expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
  2033. })
  2034. // #12448
  2035. test('work with async component', async () => {
  2036. const AsyncComp = defineAsyncComponent(() => {
  2037. return Promise.resolve({
  2038. render() {
  2039. const msg: string | undefined = inject('msg')
  2040. return h('div', {}, msg)
  2041. },
  2042. } as any)
  2043. })
  2044. const E = defineCustomElement(AsyncComp, {
  2045. configureApp(app) {
  2046. app.provide('msg', 'app-injected')
  2047. },
  2048. })
  2049. customElements.define('my-async-element-with-app', E)
  2050. container.innerHTML = `<my-async-element-with-app></my-async-element-with-app>`
  2051. const e = container.childNodes[0] as VueElement
  2052. await new Promise(r => setTimeout(r))
  2053. expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
  2054. })
  2055. test('with hmr reload', async () => {
  2056. const __hmrId = '__hmrWithApp'
  2057. const def = defineComponent({
  2058. __hmrId,
  2059. setup() {
  2060. const msg = inject('msg')
  2061. return { msg }
  2062. },
  2063. render(this: any) {
  2064. return h('div', [h('span', this.msg), h('span', this.$foo)])
  2065. },
  2066. })
  2067. const E = defineCustomElement(def, {
  2068. configureApp(app) {
  2069. app.provide('msg', 'app-injected')
  2070. app.config.globalProperties.$foo = 'foo'
  2071. },
  2072. })
  2073. customElements.define('my-element-with-app-hmr', E)
  2074. container.innerHTML = `<my-element-with-app-hmr></my-element-with-app-hmr>`
  2075. const el = container.childNodes[0] as VueElement
  2076. expect(el.shadowRoot?.innerHTML).toBe(
  2077. `<div><span>app-injected</span><span>foo</span></div>`,
  2078. )
  2079. // hmr
  2080. __VUE_HMR_RUNTIME__.reload(__hmrId, def as any)
  2081. await nextTick()
  2082. expect(el.shadowRoot?.innerHTML).toBe(
  2083. `<div><span>app-injected</span><span>foo</span></div>`,
  2084. )
  2085. })
  2086. })
  2087. // #9885
  2088. test('avoid double mount when prop is set immediately after mount', () => {
  2089. customElements.define(
  2090. 'my-input-dupe',
  2091. defineCustomElement({
  2092. props: {
  2093. value: String,
  2094. },
  2095. render() {
  2096. return 'hello'
  2097. },
  2098. }),
  2099. )
  2100. const container = document.createElement('div')
  2101. document.body.appendChild(container)
  2102. createApp({
  2103. render() {
  2104. return h('div', [
  2105. h('my-input-dupe', {
  2106. onVnodeMounted(vnode) {
  2107. vnode.el!.value = 'fesfes'
  2108. },
  2109. }),
  2110. ])
  2111. },
  2112. }).mount(container)
  2113. expect(container.children[0].children[0].shadowRoot?.innerHTML).toBe(
  2114. 'hello',
  2115. )
  2116. })
  2117. // #11081
  2118. test('Props can be casted when mounting custom elements in component rendering functions', async () => {
  2119. const E = defineCustomElement(
  2120. defineAsyncComponent(() =>
  2121. Promise.resolve({
  2122. props: ['fooValue'],
  2123. setup(props) {
  2124. expect(props.fooValue).toBe('fooValue')
  2125. return () => h('div', props.fooValue)
  2126. },
  2127. }),
  2128. ),
  2129. )
  2130. customElements.define('my-el-async-4', E)
  2131. const R = defineComponent({
  2132. setup() {
  2133. const fooValue = ref('fooValue')
  2134. return () => {
  2135. return h('div', null, [
  2136. h('my-el-async-4', {
  2137. fooValue: fooValue.value,
  2138. }),
  2139. ])
  2140. }
  2141. },
  2142. })
  2143. const app = createApp(R)
  2144. app.mount(container)
  2145. await new Promise(r => setTimeout(r))
  2146. const e = container.querySelector('my-el-async-4') as VueElement
  2147. expect(e.shadowRoot!.innerHTML).toBe(`<div>fooValue</div>`)
  2148. app.unmount()
  2149. })
  2150. // #11276
  2151. test('delete prop on attr removal', async () => {
  2152. const E = defineCustomElement({
  2153. props: {
  2154. boo: {
  2155. type: Boolean,
  2156. },
  2157. },
  2158. render() {
  2159. return this.boo + ',' + typeof this.boo
  2160. },
  2161. })
  2162. customElements.define('el-attr-removal', E)
  2163. container.innerHTML = '<el-attr-removal boo>'
  2164. const e = container.childNodes[0] as VueElement
  2165. expect(e.shadowRoot!.innerHTML).toBe(`true,boolean`)
  2166. e.removeAttribute('boo')
  2167. await nextTick()
  2168. expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`)
  2169. })
  2170. test('hyphenated attr removal', async () => {
  2171. const E = defineCustomElement({
  2172. props: {
  2173. fooBar: {
  2174. type: Boolean,
  2175. },
  2176. },
  2177. render() {
  2178. return this.fooBar
  2179. },
  2180. })
  2181. customElements.define('el-hyphenated-attr-removal', E)
  2182. const toggle = ref(true)
  2183. const Comp = {
  2184. render() {
  2185. return h('el-hyphenated-attr-removal', {
  2186. 'foo-bar': toggle.value ? '' : null,
  2187. })
  2188. },
  2189. }
  2190. render(h(Comp), container)
  2191. const el = container.children[0]
  2192. expect(el.hasAttribute('foo-bar')).toBe(true)
  2193. expect((el as any).outerHTML).toBe(
  2194. `<el-hyphenated-attr-removal foo-bar=""></el-hyphenated-attr-removal>`,
  2195. )
  2196. toggle.value = false
  2197. await nextTick()
  2198. expect(el.hasAttribute('foo-bar')).toBe(false)
  2199. expect((el as any).outerHTML).toBe(
  2200. `<el-hyphenated-attr-removal></el-hyphenated-attr-removal>`,
  2201. )
  2202. })
  2203. test('no unexpected mutation of the 1st argument', () => {
  2204. const Foo = {
  2205. name: 'Foo',
  2206. }
  2207. defineCustomElement(Foo, { shadowRoot: false })
  2208. expect(Foo).toEqual({
  2209. name: 'Foo',
  2210. })
  2211. })
  2212. })