customElement.spec.ts 74 KB

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