customElement.spec.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import {
  2. defineCustomElement,
  3. h,
  4. nextTick,
  5. ref,
  6. renderSlot,
  7. VueElement
  8. } from '../src'
  9. describe('defineCustomElement', () => {
  10. const container = document.createElement('div')
  11. document.body.appendChild(container)
  12. beforeEach(() => {
  13. container.innerHTML = ''
  14. })
  15. describe('mounting/unmount', () => {
  16. const E = defineCustomElement({
  17. render: () => h('div', 'hello')
  18. })
  19. customElements.define('my-element', E)
  20. test('should work', () => {
  21. container.innerHTML = `<my-element></my-element>`
  22. const e = container.childNodes[0] as VueElement
  23. expect(e).toBeInstanceOf(E)
  24. expect(e._instance).toBeTruthy()
  25. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  26. })
  27. test('should work w/ manual instantiation', () => {
  28. const e = new E()
  29. // should lazy init
  30. expect(e._instance).toBe(null)
  31. // should initialize on connect
  32. container.appendChild(e)
  33. expect(e._instance).toBeTruthy()
  34. expect(e.shadowRoot!.innerHTML).toBe(`<div>hello</div>`)
  35. })
  36. test('should unmount on remove', async () => {
  37. container.innerHTML = `<my-element></my-element>`
  38. const e = container.childNodes[0] as VueElement
  39. container.removeChild(e)
  40. await nextTick()
  41. expect(e._instance).toBe(null)
  42. expect(e.shadowRoot!.innerHTML).toBe('')
  43. })
  44. test('should not unmount on move', async () => {
  45. container.innerHTML = `<div><my-element></my-element></div>`
  46. const e = container.childNodes[0].childNodes[0] as VueElement
  47. const i = e._instance
  48. // moving from one parent to another - this will trigger both disconnect
  49. // and connected callbacks synchronously
  50. container.appendChild(e)
  51. await nextTick()
  52. // should be the same instance
  53. expect(e._instance).toBe(i)
  54. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div>')
  55. })
  56. })
  57. describe('props', () => {
  58. const E = defineCustomElement({
  59. props: ['foo', 'bar', 'bazQux'],
  60. render() {
  61. return [
  62. h('div', null, this.foo),
  63. h('div', null, this.bazQux || (this.bar && this.bar.x))
  64. ]
  65. }
  66. })
  67. customElements.define('my-el-props', E)
  68. test('props via attribute', async () => {
  69. // bazQux should map to `baz-qux` attribute
  70. container.innerHTML = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
  71. const e = container.childNodes[0] as VueElement
  72. expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
  73. // change attr
  74. e.setAttribute('foo', 'changed')
  75. await nextTick()
  76. expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div><div>bye</div>')
  77. e.setAttribute('baz-qux', 'changed')
  78. await nextTick()
  79. expect(e.shadowRoot!.innerHTML).toBe(
  80. '<div>changed</div><div>changed</div>'
  81. )
  82. })
  83. test('props via properties', async () => {
  84. const e = new E()
  85. e.foo = 'one'
  86. e.bar = { x: 'two' }
  87. container.appendChild(e)
  88. expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
  89. // reflect
  90. // should reflect primitive value
  91. expect(e.getAttribute('foo')).toBe('one')
  92. // should not reflect rich data
  93. expect(e.hasAttribute('bar')).toBe(false)
  94. e.foo = 'three'
  95. await nextTick()
  96. expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
  97. expect(e.getAttribute('foo')).toBe('three')
  98. e.foo = null
  99. await nextTick()
  100. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
  101. expect(e.hasAttribute('foo')).toBe(false)
  102. e.bazQux = 'four'
  103. await nextTick()
  104. expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
  105. expect(e.getAttribute('baz-qux')).toBe('four')
  106. })
  107. test('attribute -> prop type casting', async () => {
  108. const E = defineCustomElement({
  109. props: {
  110. foo: Number,
  111. bar: Boolean
  112. },
  113. render() {
  114. return [this.foo, typeof this.foo, this.bar, typeof this.bar].join(
  115. ' '
  116. )
  117. }
  118. })
  119. customElements.define('my-el-props-cast', E)
  120. container.innerHTML = `<my-el-props-cast foo="1"></my-el-props-cast>`
  121. const e = container.childNodes[0] as VueElement
  122. expect(e.shadowRoot!.innerHTML).toBe(`1 number false boolean`)
  123. e.setAttribute('bar', '')
  124. await nextTick()
  125. expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean`)
  126. e.setAttribute('foo', '2e1')
  127. await nextTick()
  128. expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean`)
  129. })
  130. test('handling properties set before upgrading', () => {
  131. const E = defineCustomElement({
  132. props: ['foo'],
  133. render() {
  134. return `foo: ${this.foo}`
  135. }
  136. })
  137. const el = document.createElement('my-el-upgrade') as any
  138. el.foo = 'hello'
  139. container.appendChild(el)
  140. customElements.define('my-el-upgrade', E)
  141. expect(el.shadowRoot.innerHTML).toBe(`foo: hello`)
  142. })
  143. })
  144. describe('emits', () => {
  145. const E = defineCustomElement({
  146. setup(_, { emit }) {
  147. emit('created')
  148. return () =>
  149. h('div', {
  150. onClick: () => emit('my-click', 1)
  151. })
  152. }
  153. })
  154. customElements.define('my-el-emits', E)
  155. test('emit on connect', () => {
  156. const e = new E()
  157. const spy = jest.fn()
  158. e.addEventListener('created', spy)
  159. container.appendChild(e)
  160. expect(spy).toHaveBeenCalled()
  161. })
  162. test('emit on interaction', () => {
  163. container.innerHTML = `<my-el-emits></my-el-emits>`
  164. const e = container.childNodes[0] as VueElement
  165. const spy = jest.fn()
  166. e.addEventListener('my-click', spy)
  167. e.shadowRoot!.childNodes[0].dispatchEvent(new CustomEvent('click'))
  168. expect(spy).toHaveBeenCalled()
  169. expect(spy.mock.calls[0][0]).toMatchObject({
  170. detail: [1]
  171. })
  172. })
  173. })
  174. describe('slots', () => {
  175. const E = defineCustomElement({
  176. render() {
  177. return [
  178. h('div', null, [
  179. renderSlot(this.$slots, 'default', undefined, () => [
  180. h('div', 'fallback')
  181. ])
  182. ]),
  183. h('div', null, renderSlot(this.$slots, 'named'))
  184. ]
  185. }
  186. })
  187. customElements.define('my-el-slots', E)
  188. test('default slot', () => {
  189. container.innerHTML = `<my-el-slots><span>hi</span></my-el-slots>`
  190. const e = container.childNodes[0] as VueElement
  191. // native slots allocation does not affect innerHTML, so we just
  192. // verify that we've rendered the correct native slots here...
  193. expect(e.shadowRoot!.innerHTML).toBe(
  194. `<div><slot><div>fallback</div></slot></div><div><slot name="named"></slot></div>`
  195. )
  196. })
  197. })
  198. describe('provide/inject', () => {
  199. const Consumer = defineCustomElement({
  200. inject: ['foo'],
  201. render(this: any) {
  202. return h('div', this.foo.value)
  203. }
  204. })
  205. customElements.define('my-consumer', Consumer)
  206. test('over nested usage', async () => {
  207. const foo = ref('injected!')
  208. const Provider = defineCustomElement({
  209. provide: {
  210. foo
  211. },
  212. render() {
  213. return h('my-consumer')
  214. }
  215. })
  216. customElements.define('my-provider', Provider)
  217. container.innerHTML = `<my-provider><my-provider>`
  218. const provider = container.childNodes[0] as VueElement
  219. const consumer = provider.shadowRoot!.childNodes[0] as VueElement
  220. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  221. foo.value = 'changed!'
  222. await nextTick()
  223. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  224. })
  225. test('over slot composition', async () => {
  226. const foo = ref('injected!')
  227. const Provider = defineCustomElement({
  228. provide: {
  229. foo
  230. },
  231. render() {
  232. return renderSlot(this.$slots, 'default')
  233. }
  234. })
  235. customElements.define('my-provider-2', Provider)
  236. container.innerHTML = `<my-provider-2><my-consumer></my-consumer><my-provider-2>`
  237. const provider = container.childNodes[0]
  238. const consumer = provider.childNodes[0] as VueElement
  239. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>injected!</div>`)
  240. foo.value = 'changed!'
  241. await nextTick()
  242. expect(consumer.shadowRoot!.innerHTML).toBe(`<div>changed!</div>`)
  243. })
  244. })
  245. })