customElement.spec.ts 80 KB

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