customElement.spec.ts 65 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127
  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, 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. // https://github.com/vuejs/core/issues/12964
  555. // Disabled because of missing support for `delegatesFocus` in jsdom
  556. // https://github.com/jsdom/jsdom/issues/3418
  557. // use vitest browser mode instead
  558. test.todo('shadowRoot should be initialized with delegatesFocus', () => {
  559. const E = defineVaporCustomElement(
  560. {
  561. setup() {
  562. return template('<input tabindex="1">', true)()
  563. },
  564. },
  565. { shadowRootOptions: { delegatesFocus: true } },
  566. )
  567. customElements.define('my-el-with-delegate-focus', E)
  568. const e = new E()
  569. container.appendChild(e)
  570. expect(e.shadowRoot!.delegatesFocus).toBe(true)
  571. })
  572. })
  573. describe('emits', () => {
  574. const CompDef = defineVaporComponent({
  575. setup(_, { emit }) {
  576. emit('created')
  577. const n0 = template('<div></div>', true)() as any
  578. n0.$evtclick = () => {
  579. emit('my-click', 1)
  580. }
  581. n0.$evtmousedown = () => {
  582. emit('myEvent', 1) // validate hyphenation
  583. }
  584. on(n0, 'wheel', () => {
  585. emit('my-wheel', { bubbles: true }, 1)
  586. })
  587. return n0
  588. },
  589. })
  590. const E = defineVaporCustomElement(CompDef)
  591. customElements.define('my-el-emits', E)
  592. test('emit on connect', () => {
  593. const e = new E()
  594. const spy = vi.fn()
  595. e.addEventListener('created', spy)
  596. container.appendChild(e)
  597. expect(spy).toHaveBeenCalled()
  598. })
  599. test('emit on interaction', () => {
  600. container.innerHTML = `<my-el-emits></my-el-emits>`
  601. const e = container.childNodes[0] as VaporElement
  602. const spy = vi.fn()
  603. e.addEventListener('my-click', spy)
  604. // Use click() method which triggers a real click event
  605. // with bubbles: true and composed: true
  606. ;(e.shadowRoot!.childNodes[0] as HTMLElement).click()
  607. expect(spy).toHaveBeenCalledTimes(1)
  608. expect(spy.mock.calls[0][0]).toMatchObject({
  609. detail: [1],
  610. })
  611. })
  612. test('case transform for camelCase event', () => {
  613. container.innerHTML = `<my-el-emits></my-el-emits>`
  614. const e = container.childNodes[0] as VaporElement
  615. const spy1 = vi.fn()
  616. e.addEventListener('myEvent', spy1)
  617. const spy2 = vi.fn()
  618. // emitting myEvent, but listening for my-event. This happens when
  619. // using the custom element in a Vue template
  620. e.addEventListener('my-event', spy2)
  621. e.shadowRoot!.childNodes[0].dispatchEvent(
  622. new CustomEvent('mousedown', {
  623. bubbles: true,
  624. composed: true,
  625. }),
  626. )
  627. expect(spy1).toHaveBeenCalledTimes(1)
  628. expect(spy2).toHaveBeenCalledTimes(1)
  629. })
  630. test('emit from within async component wrapper', async () => {
  631. const p = new Promise<typeof CompDef>(res => res(CompDef))
  632. const E = defineVaporCustomElement(defineVaporAsyncComponent(() => p))
  633. customElements.define('my-async-el-emits', E)
  634. container.innerHTML = `<my-async-el-emits></my-async-el-emits>`
  635. const e = container.childNodes[0] as VaporElement
  636. const spy = vi.fn()
  637. e.addEventListener('my-click', spy)
  638. // this feels brittle but seems necessary to reach the node in the DOM.
  639. await customElements.whenDefined('my-async-el-emits')
  640. await nextTick()
  641. await nextTick()
  642. e.shadowRoot!.childNodes[0].dispatchEvent(
  643. new CustomEvent('click', {
  644. bubbles: true,
  645. composed: true,
  646. }),
  647. )
  648. expect(spy).toHaveBeenCalled()
  649. expect(spy.mock.calls[0][0]).toMatchObject({
  650. detail: [1],
  651. })
  652. })
  653. test('emit in an async component wrapper with properties bound', async () => {
  654. const E = defineVaporCustomElement(
  655. defineVaporAsyncComponent(
  656. () => new Promise<typeof CompDef>(res => res(CompDef)),
  657. ),
  658. )
  659. customElements.define('my-async-el-props-emits', E)
  660. container.innerHTML = `<my-async-el-props-emits id="my_async_el_props_emits"></my-async-el-props-emits>`
  661. const e = container.childNodes[0] as VaporElement
  662. const spy = vi.fn()
  663. e.addEventListener('my-click', spy)
  664. await customElements.whenDefined('my-async-el-props-emits')
  665. await nextTick()
  666. await nextTick()
  667. e.shadowRoot!.childNodes[0].dispatchEvent(
  668. new CustomEvent('click', {
  669. bubbles: true,
  670. composed: true,
  671. }),
  672. )
  673. expect(spy).toHaveBeenCalled()
  674. expect(spy.mock.calls[0][0]).toMatchObject({
  675. detail: [1],
  676. })
  677. })
  678. test('emit with options', async () => {
  679. container.innerHTML = `<my-el-emits></my-el-emits>`
  680. const e = container.childNodes[0] as VaporElement
  681. const spy = vi.fn()
  682. e.addEventListener('my-wheel', spy)
  683. e.shadowRoot!.childNodes[0].dispatchEvent(
  684. new CustomEvent('wheel', {
  685. bubbles: true,
  686. composed: true,
  687. }),
  688. )
  689. expect(spy).toHaveBeenCalledTimes(1)
  690. expect(spy.mock.calls[0][0]).toMatchObject({
  691. bubbles: true,
  692. detail: [{ bubbles: true }, 1],
  693. })
  694. })
  695. })
  696. describe('slots', () => {
  697. const E = defineVaporCustomElement({
  698. setup() {
  699. const t0 = template('<div>fallback</div>')
  700. const t1 = template('<div></div>')
  701. const n3 = t1() as any
  702. setInsertionState(n3, null, true)
  703. createSlot('default', null, () => {
  704. const n2 = t0()
  705. return n2
  706. })
  707. const n5 = t1() as any
  708. setInsertionState(n5, null, true)
  709. createSlot('named', null)
  710. return [n3, n5]
  711. },
  712. })
  713. customElements.define('my-el-slots', E)
  714. test('render slots correctly', () => {
  715. container.innerHTML = `<my-el-slots><span>hi</span></my-el-slots>`
  716. const e = container.childNodes[0] as VaporElement
  717. // native slots allocation does not affect innerHTML, so we just
  718. // verify that we've rendered the correct native slots here...
  719. expect(e.shadowRoot!.innerHTML).toBe(
  720. `<div>` +
  721. `<slot><div>fallback</div></slot><!--slot-->` +
  722. `</div>` +
  723. `<div>` +
  724. `<slot name="named"></slot><!--slot-->` +
  725. `</div>`,
  726. )
  727. })
  728. test('render slot props', async () => {
  729. const foo = ref('foo')
  730. const E = defineVaporCustomElement({
  731. setup() {
  732. const n0 = template('<div></div>')() as any
  733. setInsertionState(n0, null)
  734. createSlot('default', { class: () => foo.value })
  735. return [n0]
  736. },
  737. })
  738. customElements.define('my-el-slot-props', E)
  739. container.innerHTML = `<my-el-slot-props><span>hi</span></my-el-slot-props>`
  740. const e = container.childNodes[0] as VaporElement
  741. expect(e.shadowRoot!.innerHTML).toBe(
  742. `<div><slot class="foo"></slot><!--slot--></div>`,
  743. )
  744. foo.value = 'bar'
  745. await nextTick()
  746. expect(e.shadowRoot!.innerHTML).toBe(
  747. `<div><slot class="bar"></slot><!--slot--></div>`,
  748. )
  749. })
  750. })
  751. describe('provide/inject', () => {
  752. const Consumer = defineVaporCustomElement({
  753. setup() {
  754. const foo = inject<Ref>('foo')!
  755. const n0 = template('<div> </div>', true)() as any
  756. const x0 = txt(n0) as any
  757. renderEffect(() => setText(x0, toDisplayString(foo.value)))
  758. return n0
  759. },
  760. })
  761. customElements.define('my-consumer', Consumer)
  762. test('over nested usage', async () => {
  763. const foo = ref('injected!')
  764. const Provider = defineVaporCustomElement({
  765. setup() {
  766. provide('foo', foo)
  767. return createPlainElement('my-consumer')
  768. },
  769. })
  770. customElements.define('my-provider', Provider)
  771. container.innerHTML = `<my-provider><my-provider>`
  772. const provider = container.childNodes[0] as VaporElement
  773. const consumer = provider.shadowRoot!.childNodes[0] as VaporElement
  774. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  775. foo.value = 'changed!'
  776. await nextTick()
  777. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  778. })
  779. test('over slot composition', async () => {
  780. const foo = ref('injected!')
  781. const Provider = defineVaporCustomElement({
  782. setup() {
  783. provide('foo', foo)
  784. return createSlot('default', null)
  785. },
  786. })
  787. customElements.define('my-provider-2', Provider)
  788. container.innerHTML = `<my-provider-2><my-consumer></my-consumer><my-provider-2>`
  789. const provider = container.childNodes[0]
  790. const consumer = provider.childNodes[0] as VaporElement
  791. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  792. foo.value = 'changed!'
  793. await nextTick()
  794. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  795. })
  796. test('inherited from ancestors', async () => {
  797. const fooA = ref('FooA!')
  798. const fooB = ref('FooB!')
  799. const ProviderA = defineVaporCustomElement({
  800. setup() {
  801. provide('fooA', fooA)
  802. return createPlainElement('provider-b')
  803. },
  804. })
  805. const ProviderB = defineVaporCustomElement({
  806. setup() {
  807. provide('fooB', fooB)
  808. return createPlainElement('my-multi-consumer')
  809. },
  810. })
  811. const Consumer = defineVaporCustomElement({
  812. setup() {
  813. const fooA = inject<Ref>('fooA')!
  814. const fooB = inject<Ref>('fooB')!
  815. const n0 = template('<div> </div>', true)() as any
  816. const x0 = txt(n0) as any
  817. renderEffect(() => setText(x0, `${fooA.value} ${fooB.value}`))
  818. return n0
  819. },
  820. })
  821. customElements.define('provider-a', ProviderA)
  822. customElements.define('provider-b', ProviderB)
  823. customElements.define('my-multi-consumer', Consumer)
  824. container.innerHTML = `<provider-a><provider-a>`
  825. const providerA = container.childNodes[0] as VaporElement
  826. const providerB = providerA.shadowRoot!.childNodes[0] as VaporElement
  827. const consumer = providerB.shadowRoot!.childNodes[0] as VaporElement
  828. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>FooA! FooB!</div>`)
  829. fooA.value = 'changedA!'
  830. fooB.value = 'changedB!'
  831. await nextTick()
  832. expect(consumer.shadowRoot!.innerHTML).toBe(
  833. `<div>changedA! changedB!</div>`,
  834. )
  835. })
  836. test('inherited from app context within nested elements', async () => {
  837. const outerValues: (string | undefined)[] = []
  838. const innerValues: (string | undefined)[] = []
  839. const innerChildValues: (string | undefined)[] = []
  840. const Outer = defineVaporCustomElement(
  841. {
  842. setup() {
  843. outerValues.push(
  844. inject<string>('shared'),
  845. inject<string>('outer'),
  846. inject<string>('inner'),
  847. )
  848. const n0 = template('<div></div>', true)() as any
  849. setInsertionState(n0, null)
  850. createSlot('default', null)
  851. return n0
  852. },
  853. },
  854. {
  855. configureApp(app: any) {
  856. app.provide('shared', 'shared')
  857. app.provide('outer', 'outer')
  858. },
  859. },
  860. )
  861. const Inner = defineVaporCustomElement(
  862. {
  863. setup() {
  864. // ensure values are not self-injected
  865. provide('inner', 'inner-child')
  866. innerValues.push(
  867. inject<string>('shared'),
  868. inject<string>('outer'),
  869. inject<string>('inner'),
  870. )
  871. const n0 = template('<div></div>', true)() as any
  872. setInsertionState(n0, null)
  873. createSlot('default', null)
  874. return n0
  875. },
  876. },
  877. {
  878. configureApp(app: any) {
  879. app.provide('outer', 'override-outer')
  880. app.provide('inner', 'inner')
  881. },
  882. },
  883. )
  884. const InnerChild = defineVaporCustomElement({
  885. setup() {
  886. innerChildValues.push(
  887. inject<string>('shared'),
  888. inject<string>('outer'),
  889. inject<string>('inner'),
  890. )
  891. const n0 = template('<div></div>', true)() as any
  892. return n0
  893. },
  894. })
  895. customElements.define('provide-from-app-outer', Outer)
  896. customElements.define('provide-from-app-inner', Inner)
  897. customElements.define('provide-from-app-inner-child', InnerChild)
  898. container.innerHTML =
  899. '<provide-from-app-outer>' +
  900. '<provide-from-app-inner>' +
  901. '<provide-from-app-inner-child></provide-from-app-inner-child>' +
  902. '</provide-from-app-inner>' +
  903. '</provide-from-app-outer>'
  904. const outer = container.childNodes[0] as VaporElement
  905. expect(outer.shadowRoot!.innerHTML).toBe(
  906. '<div><slot></slot><!--slot--></div>',
  907. )
  908. expect('[Vue warn]: injection "inner" not found.').toHaveBeenWarnedTimes(
  909. 1,
  910. )
  911. expect(
  912. '[Vue warn]: App already provides property with key "outer" inherited from its parent element. ' +
  913. 'It will be overwritten with the new value.',
  914. ).toHaveBeenWarnedTimes(1)
  915. expect(outerValues).toEqual(['shared', 'outer', undefined])
  916. expect(innerValues).toEqual(['shared', 'override-outer', 'inner'])
  917. expect(innerChildValues).toEqual([
  918. 'shared',
  919. 'override-outer',
  920. 'inner-child',
  921. ])
  922. })
  923. })
  924. describe('styles', () => {
  925. function assertStyles(el: VaporElement, css: string[]) {
  926. const styles = el.shadowRoot?.querySelectorAll('style')!
  927. expect(styles.length).toBe(css.length) // should not duplicate multiple copies from Bar
  928. for (let i = 0; i < css.length; i++) {
  929. expect(styles[i].textContent).toBe(css[i])
  930. }
  931. }
  932. test('should attach styles to shadow dom', async () => {
  933. const def = defineVaporComponent({
  934. __hmrId: 'foo',
  935. styles: [`div { color: red; }`],
  936. setup() {
  937. return template('<div>hello</div>', true)()
  938. },
  939. } as any)
  940. const Foo = defineVaporCustomElement(def)
  941. customElements.define('my-el-with-styles', Foo)
  942. container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
  943. const el = container.childNodes[0] as VaporElement
  944. const style = el.shadowRoot?.querySelector('style')!
  945. expect(style.textContent).toBe(`div { color: red; }`)
  946. // hmr
  947. __VUE_HMR_RUNTIME__.reload('foo', {
  948. ...def,
  949. styles: [`div { color: blue; }`, `div { color: yellow; }`],
  950. } as any)
  951. await nextTick()
  952. assertStyles(el, [`div { color: blue; }`, `div { color: yellow; }`])
  953. })
  954. test("child components should inject styles to root element's shadow root", async () => {
  955. const Baz = () => createComponent(Bar)
  956. const Bar = defineVaporComponent({
  957. __hmrId: 'bar',
  958. styles: [`div { color: green; }`, `div { color: blue; }`],
  959. setup() {
  960. return template('bar')()
  961. },
  962. } as {})
  963. const Foo = defineVaporCustomElement({
  964. styles: [`div { color: red; }`],
  965. setup() {
  966. return [createComponent(Baz), createComponent(Baz)]
  967. },
  968. })
  969. customElements.define('my-el-with-child-styles', Foo)
  970. container.innerHTML = `<my-el-with-child-styles></my-el-with-child-styles>`
  971. const el = container.childNodes[0] as VaporElement
  972. // inject order should be child -> parent
  973. assertStyles(el, [
  974. `div { color: green; }`,
  975. `div { color: blue; }`,
  976. `div { color: red; }`,
  977. ])
  978. // hmr
  979. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  980. ...Bar,
  981. styles: [`div { color: red; }`, `div { color: yellow; }`],
  982. } as any)
  983. await nextTick()
  984. assertStyles(el, [
  985. `div { color: red; }`,
  986. `div { color: yellow; }`,
  987. `div { color: red; }`,
  988. ])
  989. __VUE_HMR_RUNTIME__.reload(Bar.__hmrId!, {
  990. ...Bar,
  991. styles: [`div { color: blue; }`],
  992. } as any)
  993. await nextTick()
  994. assertStyles(el, [`div { color: blue; }`, `div { color: red; }`])
  995. })
  996. test("child components should not inject styles to root element's shadow root w/ shadowRoot false", async () => {
  997. const Bar = defineVaporComponent({
  998. styles: [`div { color: green; }`],
  999. setup() {
  1000. return template('bar')()
  1001. },
  1002. } as any)
  1003. const Baz = () => createComponent(Bar)
  1004. const Foo = defineVaporCustomElement(
  1005. {
  1006. setup() {
  1007. return [createComponent(Baz)]
  1008. },
  1009. },
  1010. { shadowRoot: false },
  1011. )
  1012. customElements.define('my-foo-with-shadowroot-false', Foo)
  1013. container.innerHTML = `<my-foo-with-shadowroot-false></my-foo-with-shadowroot-false>`
  1014. const el = container.childNodes[0] as VaporElement
  1015. const style = el.shadowRoot?.querySelector('style')
  1016. expect(style).toBeUndefined()
  1017. })
  1018. test('with nonce', () => {
  1019. const Foo = defineVaporCustomElement(
  1020. {
  1021. styles: [`div { color: red; }`],
  1022. setup() {
  1023. return template('<div>hello</div>', true)()
  1024. },
  1025. },
  1026. { nonce: 'xxx' },
  1027. )
  1028. customElements.define('my-el-with-nonce', Foo)
  1029. container.innerHTML = `<my-el-with-nonce></my-el-with-nonce>`
  1030. const el = container.childNodes[0] as VaporElement
  1031. const style = el.shadowRoot?.querySelector('style')!
  1032. expect(style.getAttribute('nonce')).toBe('xxx')
  1033. })
  1034. })
  1035. describe('async', () => {
  1036. test('should work', async () => {
  1037. const loaderSpy = vi.fn()
  1038. const E = defineVaporCustomElement(
  1039. defineVaporAsyncComponent(() => {
  1040. loaderSpy()
  1041. return Promise.resolve(
  1042. defineVaporComponent({
  1043. props: ['msg'],
  1044. styles: [`div { color: red }`],
  1045. setup(props: any) {
  1046. const n0 = template('<div> </div>', true)() as any
  1047. const x0 = txt(n0) as any
  1048. renderEffect(() => setText(x0, props.msg))
  1049. return n0
  1050. },
  1051. } as any),
  1052. )
  1053. }),
  1054. )
  1055. customElements.define('my-el-async', E)
  1056. container.innerHTML =
  1057. `<my-el-async msg="hello"></my-el-async>` +
  1058. `<my-el-async msg="world"></my-el-async>`
  1059. await new Promise(r => setTimeout(r))
  1060. // loader should be called only once
  1061. expect(loaderSpy).toHaveBeenCalledTimes(1)
  1062. const e1 = container.childNodes[0] as VaporElement
  1063. const e2 = container.childNodes[1] as VaporElement
  1064. // should inject styles
  1065. expect(e1.shadowRoot!.innerHTML).toBe(
  1066. `<style>div { color: red }</style><div>hello</div>`,
  1067. )
  1068. expect(e2.shadowRoot!.innerHTML).toBe(
  1069. `<style>div { color: red }</style><div>world</div>`,
  1070. )
  1071. // attr
  1072. e1.setAttribute('msg', 'attr')
  1073. await nextTick()
  1074. expect((e1 as any).msg).toBe('attr')
  1075. expect(e1.shadowRoot!.innerHTML).toBe(
  1076. `<style>div { color: red }</style><div>attr</div>`,
  1077. )
  1078. // props
  1079. expect(`msg` in e1).toBe(true)
  1080. ;(e1 as any).msg = 'prop'
  1081. expect(e1.getAttribute('msg')).toBe('prop')
  1082. expect(e1.shadowRoot!.innerHTML).toBe(
  1083. `<style>div { color: red }</style><div>prop</div>`,
  1084. )
  1085. })
  1086. test('set DOM property before resolve', async () => {
  1087. const E = defineVaporCustomElement(
  1088. defineVaporAsyncComponent(() => {
  1089. return Promise.resolve(
  1090. defineVaporComponent({
  1091. props: ['msg'],
  1092. setup(props: any) {
  1093. expect(typeof props.msg).toBe('string')
  1094. const n0 = template('<div> </div>', true)() as any
  1095. const x0 = txt(n0) as any
  1096. renderEffect(() => setText(x0, props.msg))
  1097. return n0
  1098. },
  1099. }),
  1100. )
  1101. }),
  1102. )
  1103. customElements.define('my-el-async-2', E)
  1104. const e1 = new E() as any
  1105. // set property before connect
  1106. e1.msg = 'hello'
  1107. const e2 = new E() as any
  1108. container.appendChild(e1)
  1109. container.appendChild(e2)
  1110. // set property after connect but before resolve
  1111. e2.msg = 'world'
  1112. await new Promise(r => setTimeout(r))
  1113. expect(e1.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1114. expect(e2.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1115. e1.msg = 'world'
  1116. expect(e1.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1117. e2.msg = 'hello'
  1118. expect(e2.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1119. })
  1120. test('Number prop casting before resolve', async () => {
  1121. const E = defineVaporCustomElement(
  1122. defineVaporAsyncComponent(() => {
  1123. return Promise.resolve(
  1124. defineVaporComponent({
  1125. props: { n: Number },
  1126. setup(props: any) {
  1127. expect(props.n).toBe(20)
  1128. const n0 = template('<div> </div>', true)() as any
  1129. const x0 = txt(n0) as any
  1130. renderEffect(() => setText(x0, `${props.n},${typeof props.n}`))
  1131. return n0
  1132. },
  1133. }),
  1134. )
  1135. }),
  1136. )
  1137. customElements.define('my-el-async-3', E)
  1138. container.innerHTML = `<my-el-async-3 n="2e1"></my-el-async-3>`
  1139. await new Promise(r => setTimeout(r))
  1140. const e = container.childNodes[0] as VaporElement
  1141. expect(e.shadowRoot!.innerHTML).toBe(`<div>20,number</div>`)
  1142. })
  1143. test('with slots', async () => {
  1144. const E = defineVaporCustomElement(
  1145. defineVaporAsyncComponent(() => {
  1146. return Promise.resolve(
  1147. defineVaporComponent({
  1148. setup() {
  1149. const t0 = template('<div>fallback</div>')
  1150. const t1 = template('<div></div>')
  1151. const n3 = t1() as any
  1152. setInsertionState(n3, null)
  1153. createSlot('default', null, () => {
  1154. const n2 = t0()
  1155. return n2
  1156. })
  1157. const n5 = t1() as any
  1158. setInsertionState(n5, null)
  1159. createSlot('named', null)
  1160. return [n3, n5]
  1161. },
  1162. }),
  1163. )
  1164. }),
  1165. )
  1166. customElements.define('my-el-async-slots', E)
  1167. container.innerHTML = `<my-el-async-slots><span>hi</span></my-el-async-slots>`
  1168. await new Promise(r => setTimeout(r))
  1169. const e = container.childNodes[0] as VaporElement
  1170. expect(e.shadowRoot!.innerHTML).toBe(
  1171. `<div>` +
  1172. `<slot><div>fallback</div></slot><!--slot-->` +
  1173. `</div><div>` +
  1174. `<slot name="named"></slot><!--slot-->` +
  1175. `</div>`,
  1176. )
  1177. })
  1178. })
  1179. describe('shadowRoot: false', () => {
  1180. const E = defineVaporCustomElement({
  1181. shadowRoot: false,
  1182. props: {
  1183. msg: {
  1184. type: String,
  1185. default: 'hello',
  1186. },
  1187. },
  1188. setup(props: any) {
  1189. const n0 = template('<div> </div>')() as any
  1190. const x0 = txt(n0) as any
  1191. renderEffect(() => setText(x0, toDisplayString(props.msg)))
  1192. return n0
  1193. },
  1194. })
  1195. customElements.define('my-el-shadowroot-false', E)
  1196. test('should work', async () => {
  1197. function raf() {
  1198. return new Promise(resolve => {
  1199. requestAnimationFrame(resolve)
  1200. })
  1201. }
  1202. container.innerHTML = `<my-el-shadowroot-false></my-el-shadowroot-false>`
  1203. const e = container.childNodes[0] as VaporElement
  1204. await raf()
  1205. expect(e).toBeInstanceOf(E)
  1206. expect(e._instance).toBeTruthy()
  1207. expect(e.innerHTML).toBe(`<div>hello</div>`)
  1208. expect(e.shadowRoot).toBe(null)
  1209. })
  1210. const toggle = ref(true)
  1211. const ES = defineVaporCustomElement(
  1212. {
  1213. setup() {
  1214. const n0 = createSlot('default')
  1215. const n1 = createIf(
  1216. () => toggle.value,
  1217. () => createSlot('named'),
  1218. )
  1219. const n2 = createSlot('omitted', null, () =>
  1220. template('<div>fallback</div>')(),
  1221. )
  1222. return [n0, n1, n2]
  1223. },
  1224. },
  1225. { shadowRoot: false },
  1226. )
  1227. customElements.define('my-el-shadowroot-false-slots', ES)
  1228. test('should render slots', async () => {
  1229. container.innerHTML =
  1230. `<my-el-shadowroot-false-slots>` +
  1231. `<span>default</span>text` +
  1232. `<div slot="named">named</div>` +
  1233. `</my-el-shadowroot-false-slots>`
  1234. const e = container.childNodes[0] as VaporElement
  1235. // native slots allocation does not affect innerHTML, so we just
  1236. // verify that we've rendered the correct native slots here...
  1237. expect(e.innerHTML).toBe(
  1238. `<span>default</span>text<!--slot-->` +
  1239. `<div slot="named">named</div><!--slot--><!--if-->` +
  1240. `<div>fallback</div><!--slot-->`,
  1241. )
  1242. toggle.value = false
  1243. await nextTick()
  1244. expect(e.innerHTML).toBe(
  1245. `<span>default</span>text<!--slot-->` +
  1246. `<!--if-->` +
  1247. `<div>fallback</div><!--slot-->`,
  1248. )
  1249. })
  1250. test('render nested customElement w/ shadowRoot false', async () => {
  1251. const calls: string[] = []
  1252. const Child = defineVaporCustomElement(
  1253. {
  1254. setup() {
  1255. calls.push('child rendering')
  1256. onMounted(() => {
  1257. calls.push('child mounted')
  1258. })
  1259. return createSlot('default')
  1260. },
  1261. },
  1262. { shadowRoot: false },
  1263. )
  1264. customElements.define('my-child', Child)
  1265. const Parent = defineVaporCustomElement(
  1266. {
  1267. setup() {
  1268. calls.push('parent rendering')
  1269. onMounted(() => {
  1270. calls.push('parent mounted')
  1271. })
  1272. return createSlot('default')
  1273. },
  1274. },
  1275. { shadowRoot: false },
  1276. )
  1277. customElements.define('my-parent', Parent)
  1278. const App = {
  1279. setup() {
  1280. return createPlainElement('my-parent', null, {
  1281. default: withVaporCtx(() =>
  1282. createPlainElement('my-child', null, {
  1283. default: () => template('<span>default</span>')(),
  1284. }),
  1285. ),
  1286. })
  1287. },
  1288. }
  1289. const app = createVaporApp(App)
  1290. app.mount(container)
  1291. await nextTick()
  1292. const e = container.childNodes[0] as VaporElement
  1293. expect(e.innerHTML).toBe(
  1294. `<my-child data-v-app=""><span>default</span><!--slot--></my-child><!--slot-->`,
  1295. )
  1296. expect(calls).toEqual([
  1297. 'parent rendering',
  1298. 'parent mounted',
  1299. 'child rendering',
  1300. 'child mounted',
  1301. ])
  1302. app.unmount()
  1303. })
  1304. test('render nested Teleport w/ shadowRoot false', async () => {
  1305. const target = document.createElement('div')
  1306. const Child = defineVaporCustomElement(
  1307. {
  1308. setup() {
  1309. return createComponent(
  1310. VaporTeleport,
  1311. { to: () => target },
  1312. {
  1313. default: withVaporCtx(() => createSlot('default')),
  1314. },
  1315. )
  1316. },
  1317. },
  1318. { shadowRoot: false },
  1319. )
  1320. customElements.define('my-el-teleport-child', Child)
  1321. const Parent = defineVaporCustomElement(
  1322. {
  1323. setup() {
  1324. return createSlot('default')
  1325. },
  1326. },
  1327. { shadowRoot: false },
  1328. )
  1329. customElements.define('my-el-teleport-parent', Parent)
  1330. const App = {
  1331. setup() {
  1332. return createPlainElement('my-el-teleport-parent', null, {
  1333. default: withVaporCtx(() =>
  1334. createPlainElement('my-el-teleport-child', null, {
  1335. default: () => template('<span>default</span>')(),
  1336. }),
  1337. ),
  1338. })
  1339. },
  1340. }
  1341. const app = createVaporApp(App)
  1342. app.mount(container)
  1343. await nextTick()
  1344. expect(target.innerHTML).toBe(`<span>default</span><!--slot-->`)
  1345. app.unmount()
  1346. })
  1347. test('render two Teleports w/ shadowRoot false', async () => {
  1348. const target1 = document.createElement('div')
  1349. const target2 = document.createElement('span')
  1350. const Child = defineVaporCustomElement(
  1351. {
  1352. setup() {
  1353. return [
  1354. createComponent(
  1355. VaporTeleport,
  1356. { to: () => target1 },
  1357. {
  1358. default: withVaporCtx(() => createSlot('header')),
  1359. },
  1360. ),
  1361. createComponent(
  1362. VaporTeleport,
  1363. { to: () => target2 },
  1364. {
  1365. default: withVaporCtx(() => createSlot('body')),
  1366. },
  1367. ),
  1368. ]
  1369. },
  1370. },
  1371. { shadowRoot: false },
  1372. )
  1373. customElements.define('my-el-two-teleport-child', Child)
  1374. const App = {
  1375. setup() {
  1376. return createPlainElement('my-el-two-teleport-child', null, {
  1377. default: () => [
  1378. template('<div slot="header">header</div>')(),
  1379. template('<span slot="body">body</span>')(),
  1380. ],
  1381. })
  1382. },
  1383. }
  1384. const app = createVaporApp(App)
  1385. app.mount(container)
  1386. await nextTick()
  1387. expect(target1.outerHTML).toBe(
  1388. `<div><div slot="header">header</div><!--slot--></div>`,
  1389. )
  1390. expect(target2.outerHTML).toBe(
  1391. `<span><span slot="body">body</span><!--slot--></span>`,
  1392. )
  1393. app.unmount()
  1394. })
  1395. test('render two Teleports w/ shadowRoot false (with disabled)', async () => {
  1396. const target1 = document.createElement('div')
  1397. const target2 = document.createElement('span')
  1398. const Child = defineVaporCustomElement(
  1399. {
  1400. setup() {
  1401. return [
  1402. createComponent(
  1403. VaporTeleport,
  1404. // with disabled: true
  1405. { to: () => target1, disabled: () => true },
  1406. {
  1407. default: withVaporCtx(() => createSlot('header')),
  1408. },
  1409. ),
  1410. createComponent(
  1411. VaporTeleport,
  1412. { to: () => target2 },
  1413. {
  1414. default: withVaporCtx(() => createSlot('body')),
  1415. },
  1416. ),
  1417. ]
  1418. },
  1419. },
  1420. { shadowRoot: false },
  1421. )
  1422. customElements.define('my-el-two-teleport-child-0', Child)
  1423. const App = {
  1424. setup() {
  1425. return createPlainElement('my-el-two-teleport-child-0', null, {
  1426. default: () => [
  1427. template('<div slot="header">header</div>')(),
  1428. template('<span slot="body">body</span>')(),
  1429. ],
  1430. })
  1431. },
  1432. }
  1433. const app = createVaporApp(App)
  1434. app.mount(container)
  1435. await nextTick()
  1436. expect(target1.outerHTML).toBe(`<div></div>`)
  1437. expect(target2.outerHTML).toBe(
  1438. `<span><span slot="body">body</span><!--slot--></span>`,
  1439. )
  1440. app.unmount()
  1441. })
  1442. test('toggle nested custom element with shadowRoot: false', async () => {
  1443. customElements.define(
  1444. 'my-el-child-shadow-false',
  1445. defineVaporCustomElement(
  1446. {
  1447. setup() {
  1448. const n0 = template('<div></div>')() as any
  1449. setInsertionState(n0, null)
  1450. createSlot('default', null)
  1451. return n0
  1452. },
  1453. },
  1454. { shadowRoot: false },
  1455. ),
  1456. )
  1457. const ChildWrapper = {
  1458. setup() {
  1459. return createPlainElement('my-el-child-shadow-false', null, {
  1460. default: () => template('child')(),
  1461. })
  1462. },
  1463. }
  1464. customElements.define(
  1465. 'my-el-parent-shadow-false',
  1466. defineVaporCustomElement(
  1467. {
  1468. props: {
  1469. isShown: { type: Boolean, required: true },
  1470. },
  1471. setup(props: any) {
  1472. return createIf(
  1473. () => props.isShown,
  1474. () => {
  1475. const n0 = template('<div></div>')() as any
  1476. setInsertionState(n0, null)
  1477. createSlot('default', null)
  1478. return n0
  1479. },
  1480. )
  1481. },
  1482. },
  1483. { shadowRoot: false },
  1484. ),
  1485. )
  1486. const ParentWrapper = {
  1487. props: {
  1488. isShown: { type: Boolean, required: true },
  1489. },
  1490. setup(props: any) {
  1491. return createPlainElement(
  1492. 'my-el-parent-shadow-false',
  1493. { isShown: () => props.isShown },
  1494. {
  1495. default: withVaporCtx(() => createSlot('default')),
  1496. },
  1497. )
  1498. },
  1499. }
  1500. const isShown = ref(true)
  1501. const App = {
  1502. setup() {
  1503. return createComponent(
  1504. ParentWrapper,
  1505. { isShown: () => isShown.value },
  1506. {
  1507. default: withVaporCtx(() => createComponent(ChildWrapper)),
  1508. },
  1509. )
  1510. },
  1511. }
  1512. const container = document.createElement('div')
  1513. document.body.appendChild(container)
  1514. const app = createVaporApp(App)
  1515. app.mount(container)
  1516. expect(container.innerHTML).toBe(
  1517. `<my-el-parent-shadow-false is-shown="" data-v-app="">` +
  1518. `<div>` +
  1519. `<my-el-child-shadow-false data-v-app="">` +
  1520. `<div>child<!--slot--></div>` +
  1521. `</my-el-child-shadow-false><!--slot--><!--slot-->` +
  1522. `</div><!--if-->` +
  1523. `</my-el-parent-shadow-false>`,
  1524. )
  1525. isShown.value = false
  1526. await nextTick()
  1527. expect(container.innerHTML).toBe(
  1528. `<my-el-parent-shadow-false data-v-app=""><!--if--></my-el-parent-shadow-false>`,
  1529. )
  1530. isShown.value = true
  1531. await nextTick()
  1532. expect(container.innerHTML).toBe(
  1533. `<my-el-parent-shadow-false data-v-app="" is-shown="">` +
  1534. `<div>` +
  1535. `<my-el-child-shadow-false data-v-app="">` +
  1536. `<div>child<!--slot--></div>` +
  1537. `</my-el-child-shadow-false><!--slot--><!--slot-->` +
  1538. `</div><!--if-->` +
  1539. `</my-el-parent-shadow-false>`,
  1540. )
  1541. })
  1542. })
  1543. describe('helpers', () => {
  1544. test('useHost', () => {
  1545. const Foo = defineVaporCustomElement({
  1546. setup() {
  1547. const host = useHost()!
  1548. host.setAttribute('id', 'host')
  1549. return template('<div>hello</div>')()
  1550. },
  1551. })
  1552. customElements.define('my-el-use-host', Foo)
  1553. container.innerHTML = `<my-el-use-host>`
  1554. const el = container.childNodes[0] as VaporElement
  1555. expect(el.id).toBe('host')
  1556. })
  1557. test('useShadowRoot for style injection', () => {
  1558. const Foo = defineVaporCustomElement({
  1559. setup() {
  1560. const root = useShadowRoot()!
  1561. const style = document.createElement('style')
  1562. style.innerHTML = `div { color: red; }`
  1563. root.appendChild(style)
  1564. return template('<div>hello</div>')()
  1565. },
  1566. })
  1567. customElements.define('my-el-use-shadow-root', Foo)
  1568. container.innerHTML = `<my-el-use-shadow-root>`
  1569. const el = container.childNodes[0] as VaporElement
  1570. const style = el.shadowRoot?.querySelector('style')!
  1571. expect(style.textContent).toBe(`div { color: red; }`)
  1572. })
  1573. })
  1574. describe('expose', () => {
  1575. test('expose w/ options api', async () => {
  1576. const E = defineVaporCustomElement({
  1577. setup(_: any, { expose }: any) {
  1578. const value = ref(0)
  1579. const foo = () => {
  1580. value.value++
  1581. }
  1582. expose({ foo })
  1583. const n0 = template('<div> </div>', true)() as any
  1584. const x0 = txt(n0) as any
  1585. renderEffect(() => setText(x0, `${value.value}`))
  1586. return n0
  1587. },
  1588. })
  1589. customElements.define('my-el-expose-options-api', E)
  1590. container.innerHTML = `<my-el-expose-options-api></my-el-expose-options-api>`
  1591. const e = container.childNodes[0] as VaporElement & {
  1592. foo: () => void
  1593. }
  1594. expect(e.shadowRoot!.innerHTML).toBe(`<div>0</div>`)
  1595. e.foo()
  1596. await nextTick()
  1597. expect(e.shadowRoot!.innerHTML).toBe(`<div>1</div>`)
  1598. })
  1599. test('expose attributes and callback', async () => {
  1600. type SetValue = (value: string) => void
  1601. let fn: MockedFunction<SetValue>
  1602. const E = defineVaporCustomElement({
  1603. setup(_: any, { expose }: any) {
  1604. const value = ref('hello')
  1605. const setValue = (fn = vi.fn((_value: string) => {
  1606. value.value = _value
  1607. }))
  1608. expose({
  1609. setValue,
  1610. value,
  1611. })
  1612. const n0 = template('<div> </div>', true)() as any
  1613. const x0 = txt(n0) as any
  1614. renderEffect(() => setText(x0, value.value))
  1615. return n0
  1616. },
  1617. })
  1618. customElements.define('my-el-expose', E)
  1619. container.innerHTML = `<my-el-expose></my-el-expose>`
  1620. const e = container.childNodes[0] as VaporElement & {
  1621. value: string
  1622. setValue: MockedFunction<SetValue>
  1623. }
  1624. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  1625. expect(e.value).toBe('hello')
  1626. expect(e.setValue).toBe(fn!)
  1627. e.setValue('world')
  1628. expect(e.value).toBe('world')
  1629. await nextTick()
  1630. expect(e.shadowRoot!.innerHTML).toBe(`<div>world</div>`)
  1631. })
  1632. test('warning when exposing an existing property', () => {
  1633. const E = defineVaporCustomElement({
  1634. props: {
  1635. value: String,
  1636. },
  1637. setup(props: any, { expose }: any) {
  1638. expose({
  1639. value: 'hello',
  1640. })
  1641. const n0 = template('<div> </div>', true)() as any
  1642. const x0 = txt(n0) as any
  1643. renderEffect(() => setText(x0, props.value))
  1644. return n0
  1645. },
  1646. })
  1647. customElements.define('my-el-expose-two', E)
  1648. container.innerHTML = `<my-el-expose-two value="world"></my-el-expose-two>`
  1649. expect(
  1650. `[Vue warn]: Exposed property "value" already exists on custom element.`,
  1651. ).toHaveBeenWarned()
  1652. })
  1653. })
  1654. test('async & nested custom elements', async () => {
  1655. let fooVal: string | undefined = ''
  1656. const E = defineVaporCustomElement(
  1657. defineVaporAsyncComponent(() => {
  1658. return Promise.resolve(
  1659. defineVaporComponent({
  1660. setup() {
  1661. provide('foo', 'foo')
  1662. const n0 = template('<div></div>')() as any
  1663. setInsertionState(n0, null)
  1664. createSlot('default', null)
  1665. return n0
  1666. },
  1667. }),
  1668. )
  1669. }),
  1670. )
  1671. const EChild = defineVaporCustomElement({
  1672. setup() {
  1673. fooVal = inject('foo')
  1674. const n0 = template('<div>child</div>')()
  1675. return n0
  1676. },
  1677. })
  1678. customElements.define('my-el-async-nested-ce', E)
  1679. customElements.define('slotted-child', EChild)
  1680. container.innerHTML = `<my-el-async-nested-ce><div><slotted-child></slotted-child></div></my-el-async-nested-ce>`
  1681. await new Promise(r => setTimeout(r))
  1682. const e = container.childNodes[0] as VaporElement
  1683. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot><!--slot--></div>`)
  1684. expect(fooVal).toBe('foo')
  1685. })
  1686. test('async & multiple levels of nested custom elements', async () => {
  1687. let fooVal: string | undefined = ''
  1688. let barVal: string | undefined = ''
  1689. const E = defineVaporCustomElement(
  1690. defineVaporAsyncComponent(() => {
  1691. return Promise.resolve(
  1692. defineVaporComponent({
  1693. setup() {
  1694. provide('foo', 'foo')
  1695. const n0 = template('<div></div>')() as any
  1696. setInsertionState(n0, null)
  1697. createSlot('default', null)
  1698. return n0
  1699. },
  1700. }),
  1701. )
  1702. }),
  1703. )
  1704. const EChild = defineVaporCustomElement({
  1705. setup() {
  1706. provide('bar', 'bar')
  1707. const n0 = template('<div></div>')() as any
  1708. setInsertionState(n0, null)
  1709. createSlot('default', null)
  1710. return n0
  1711. },
  1712. })
  1713. const EChild2 = defineVaporCustomElement({
  1714. setup() {
  1715. fooVal = inject('foo')
  1716. barVal = inject('bar')
  1717. const n0 = template('<div>child</div>')()
  1718. return n0
  1719. },
  1720. })
  1721. customElements.define('my-el-async-nested-m-ce', E)
  1722. customElements.define('slotted-child-m', EChild)
  1723. customElements.define('slotted-child2-m', EChild2)
  1724. container.innerHTML =
  1725. `<my-el-async-nested-m-ce>` +
  1726. `<div><slotted-child-m>` +
  1727. `<slotted-child2-m></slotted-child2-m>` +
  1728. `</slotted-child-m></div>` +
  1729. `</my-el-async-nested-m-ce>`
  1730. await new Promise(r => setTimeout(r))
  1731. const e = container.childNodes[0] as VaporElement
  1732. expect(e.shadowRoot!.innerHTML).toBe(`<div><slot></slot><!--slot--></div>`)
  1733. expect(fooVal).toBe('foo')
  1734. expect(barVal).toBe('bar')
  1735. })
  1736. describe('configureApp', () => {
  1737. test('should work', () => {
  1738. const E = defineVaporCustomElement(
  1739. () => {
  1740. const msg = inject('msg')
  1741. const n0 = template('<div> </div>', true)() as any
  1742. const x0 = txt(n0) as any
  1743. renderEffect(() => setText(x0, msg as string))
  1744. return n0
  1745. },
  1746. {
  1747. configureApp(app: any) {
  1748. app.provide('msg', 'app-injected')
  1749. },
  1750. },
  1751. )
  1752. customElements.define('my-element-with-app', E)
  1753. container.innerHTML = `<my-element-with-app></my-element-with-app>`
  1754. const e = container.childNodes[0] as VaporElement
  1755. expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
  1756. })
  1757. test('work with async component', async () => {
  1758. const AsyncComp = defineVaporAsyncComponent(() => {
  1759. return Promise.resolve(
  1760. defineVaporComponent({
  1761. setup() {
  1762. const msg = inject('msg')
  1763. const n0 = template('<div> </div>', true)() as any
  1764. const x0 = txt(n0) as any
  1765. renderEffect(() => setText(x0, msg as string))
  1766. return n0
  1767. },
  1768. }),
  1769. )
  1770. })
  1771. const E = defineVaporCustomElement(AsyncComp, {
  1772. configureApp(app: any) {
  1773. app.provide('msg', 'app-injected')
  1774. },
  1775. })
  1776. customElements.define('my-async-element-with-app', E)
  1777. container.innerHTML = `<my-async-element-with-app></my-async-element-with-app>`
  1778. const e = container.childNodes[0] as VaporElement
  1779. await new Promise(r => setTimeout(r))
  1780. expect(e.shadowRoot?.innerHTML).toBe('<div>app-injected</div>')
  1781. })
  1782. test('with hmr reload', async () => {
  1783. const __hmrId = '__hmrWithApp'
  1784. const def = defineVaporComponent({
  1785. __hmrId,
  1786. setup() {
  1787. const msg = inject('msg')
  1788. const n0 = template('<div><span> </span></div>')() as any
  1789. const n1 = child(n0) as any
  1790. const x1 = txt(n1) as any
  1791. renderEffect(() => setText(x1, msg as string))
  1792. return n0
  1793. },
  1794. })
  1795. const E = defineVaporCustomElement(def, {
  1796. configureApp(app: any) {
  1797. app.provide('msg', 'app-injected')
  1798. },
  1799. })
  1800. customElements.define('my-element-with-app-hmr', E)
  1801. container.innerHTML = `<my-element-with-app-hmr></my-element-with-app-hmr>`
  1802. const el = container.childNodes[0] as VaporElement
  1803. expect(el.shadowRoot?.innerHTML).toBe(
  1804. `<div><span>app-injected</span></div>`,
  1805. )
  1806. // hmr
  1807. __VUE_HMR_RUNTIME__.reload(__hmrId, def as any)
  1808. await nextTick()
  1809. expect(el.shadowRoot?.innerHTML).toBe(
  1810. `<div><span>app-injected</span></div>`,
  1811. )
  1812. })
  1813. })
  1814. // #9885
  1815. // test('avoid double mount when prop is set immediately after mount', () => {
  1816. // customElements.define(
  1817. // 'my-input-dupe',
  1818. // defineVaporCustomElement({
  1819. // props: {
  1820. // value: String,
  1821. // },
  1822. // render() {
  1823. // return 'hello'
  1824. // },
  1825. // }),
  1826. // )
  1827. // const container = document.createElement('div')
  1828. // document.body.appendChild(container)
  1829. // createVaporApp({
  1830. // // render() {
  1831. // // return h('div', [
  1832. // // h('my-input-dupe', {
  1833. // // onVnodeMounted(vnode) {
  1834. // // vnode.el!.value = 'fesfes'
  1835. // // },
  1836. // // }),
  1837. // // ])
  1838. // // },
  1839. // setup() {
  1840. // // const n0 = template('<div></div>')() as any
  1841. // }
  1842. // }).mount(container)
  1843. // expect(container.children[0].children[0].shadowRoot?.innerHTML).toBe(
  1844. // 'hello',
  1845. // )
  1846. // })
  1847. test('Props can be casted when mounting custom elements in component rendering functions', async () => {
  1848. const E = defineVaporCustomElement(
  1849. defineVaporAsyncComponent(() =>
  1850. Promise.resolve(
  1851. defineVaporComponent({
  1852. props: ['fooValue'],
  1853. setup(props: any) {
  1854. expect(props.fooValue).toBe('fooValue')
  1855. const n0 = template('<div> </div>', true)() as any
  1856. const x0 = txt(n0) as any
  1857. renderEffect(() => setText(x0, props.fooValue))
  1858. return n0
  1859. },
  1860. }),
  1861. ),
  1862. ),
  1863. )
  1864. customElements.define('my-el-async-4', E)
  1865. const R = defineVaporComponent({
  1866. setup() {
  1867. const fooValue = ref('fooValue')
  1868. const n0 = template('<div></div>')() as any
  1869. setInsertionState(n0, null)
  1870. createPlainElement('my-el-async-4', {
  1871. fooValue: () => fooValue.value,
  1872. })
  1873. return n0
  1874. },
  1875. })
  1876. const app = createVaporApp(R)
  1877. app.mount(container)
  1878. await new Promise(r => setTimeout(r))
  1879. const e = container.querySelector('my-el-async-4') as VaporElement
  1880. expect(e.shadowRoot!.innerHTML).toBe(`<div>fooValue</div>`)
  1881. app.unmount()
  1882. })
  1883. test('delete prop on attr removal', async () => {
  1884. const E = defineVaporCustomElement({
  1885. props: {
  1886. boo: {
  1887. type: Boolean,
  1888. },
  1889. },
  1890. setup(props: any) {
  1891. const n0 = template(' ')() as any
  1892. renderEffect(() => setText(n0, `${props.boo},${typeof props.boo}`))
  1893. return n0
  1894. },
  1895. })
  1896. customElements.define('el-attr-removal', E)
  1897. container.innerHTML = '<el-attr-removal boo>'
  1898. const e = container.childNodes[0] as VaporElement
  1899. expect(e.shadowRoot!.innerHTML).toBe(`true,boolean`)
  1900. e.removeAttribute('boo')
  1901. await nextTick()
  1902. expect(e.shadowRoot!.innerHTML).toBe(`false,boolean`)
  1903. })
  1904. test('hyphenated attr removal', async () => {
  1905. const E = defineVaporCustomElement({
  1906. props: {
  1907. fooBar: {
  1908. type: Boolean,
  1909. },
  1910. },
  1911. setup(props: any) {
  1912. const n0 = template(' ')() as any
  1913. renderEffect(() => setText(n0, toDisplayString(props.fooBar)))
  1914. return n0
  1915. },
  1916. })
  1917. customElements.define('el-hyphenated-attr-removal', E)
  1918. const toggle = ref(true)
  1919. const { container } = render('el-hyphenated-attr-removal', {
  1920. 'foo-bar': () => (toggle.value ? '' : null),
  1921. })
  1922. const el = container.children[0]
  1923. expect(el.hasAttribute('foo-bar')).toBe(true)
  1924. expect((el as any).outerHTML).toBe(
  1925. `<el-hyphenated-attr-removal foo-bar=""></el-hyphenated-attr-removal>`,
  1926. )
  1927. toggle.value = false
  1928. await nextTick()
  1929. expect(el.hasAttribute('foo-bar')).toBe(false)
  1930. expect((el as any).outerHTML).toBe(
  1931. `<el-hyphenated-attr-removal></el-hyphenated-attr-removal>`,
  1932. )
  1933. })
  1934. test('no unexpected mutation of the 1st argument', () => {
  1935. const Foo = {
  1936. __vapor: true,
  1937. name: 'Foo',
  1938. }
  1939. defineVaporCustomElement(Foo, { shadowRoot: false })
  1940. expect(Foo).toEqual({
  1941. __vapor: true,
  1942. name: 'Foo',
  1943. })
  1944. })
  1945. })