componentAttrs.spec.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. import { type Ref, nextTick, ref } from '@vue/runtime-dom'
  2. import {
  3. createComponent,
  4. defineVaporComponent,
  5. renderEffect,
  6. setClass,
  7. setDynamicProps,
  8. setProp,
  9. setStyle,
  10. template,
  11. } from '../src'
  12. import { makeRender } from './_utils'
  13. import { stringifyStyle } from '@vue/shared'
  14. import { setElementText } from '../src/dom/prop'
  15. const define = makeRender<any>()
  16. // TODO: port more tests from rendererAttrsFallthrough.spec.ts
  17. describe('attribute fallthrough', () => {
  18. it('should allow attrs to fallthrough', async () => {
  19. const t0 = template('<div>', true)
  20. const { component: Child } = define({
  21. props: ['foo'],
  22. setup(props: any) {
  23. const n0 = t0() as Element
  24. renderEffect(() => setElementText(n0, props.foo))
  25. return n0
  26. },
  27. })
  28. const foo = ref(1)
  29. const id = ref('a')
  30. const { host } = define({
  31. setup() {
  32. return createComponent(
  33. Child,
  34. {
  35. foo: () => foo.value,
  36. id: () => id.value,
  37. },
  38. null,
  39. true,
  40. )
  41. },
  42. }).render()
  43. expect(host.innerHTML).toBe('<div id="a">1</div>')
  44. foo.value++
  45. await nextTick()
  46. expect(host.innerHTML).toBe('<div id="a">2</div>')
  47. id.value = 'b'
  48. await nextTick()
  49. expect(host.innerHTML).toBe('<div id="b">2</div>')
  50. })
  51. it('should not fallthrough if explicitly pass inheritAttrs: false', async () => {
  52. const t0 = template('<div>', true)
  53. const { component: Child } = define({
  54. props: ['foo'],
  55. inheritAttrs: false,
  56. setup(props: any) {
  57. const n0 = t0() as Element
  58. renderEffect(() => setElementText(n0, props.foo))
  59. return n0
  60. },
  61. })
  62. const foo = ref(1)
  63. const id = ref('a')
  64. const { host } = define({
  65. setup() {
  66. return createComponent(
  67. Child,
  68. {
  69. foo: () => foo.value,
  70. id: () => id.value,
  71. },
  72. null,
  73. true,
  74. )
  75. },
  76. }).render()
  77. expect(host.innerHTML).toBe('<div>1</div>')
  78. foo.value++
  79. await nextTick()
  80. expect(host.innerHTML).toBe('<div>2</div>')
  81. id.value = 'b'
  82. await nextTick()
  83. expect(host.innerHTML).toBe('<div>2</div>')
  84. })
  85. it('should pass through attrs in nested single root components', async () => {
  86. const t0 = template('<div>', true)
  87. const { component: Grandson } = define({
  88. props: ['custom-attr'],
  89. setup(_: any, { attrs }: any) {
  90. const n0 = t0() as Element
  91. renderEffect(() => setElementText(n0, attrs.foo))
  92. return n0
  93. },
  94. })
  95. const { component: Child } = define({
  96. setup() {
  97. const n0 = createComponent(
  98. Grandson,
  99. {
  100. 'custom-attr': () => 'custom-attr',
  101. },
  102. null,
  103. true,
  104. )
  105. return n0
  106. },
  107. })
  108. const foo = ref(1)
  109. const id = ref('a')
  110. const { host } = define({
  111. setup() {
  112. return createComponent(
  113. Child,
  114. {
  115. foo: () => foo.value,
  116. id: () => id.value,
  117. },
  118. null,
  119. true,
  120. )
  121. },
  122. }).render()
  123. expect(host.innerHTML).toBe('<div foo="1" id="a">1</div>')
  124. foo.value++
  125. await nextTick()
  126. expect(host.innerHTML).toBe('<div foo="2" id="a">2</div>')
  127. id.value = 'b'
  128. await nextTick()
  129. expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
  130. })
  131. it('should merge classes', async () => {
  132. const rootClass = ref('root')
  133. const parentClass = ref('parent')
  134. const childClass = ref('child')
  135. const t0 = template('<div>', true /* root */)
  136. const Child = defineVaporComponent({
  137. setup() {
  138. const n = t0() as Element
  139. renderEffect(() => {
  140. // binding on template root generates incremental class setter
  141. setClass(n, childClass.value)
  142. })
  143. return n
  144. },
  145. })
  146. const Parent = defineVaporComponent({
  147. setup() {
  148. return createComponent(
  149. Child,
  150. {
  151. class: () => parentClass.value,
  152. },
  153. null,
  154. true, // pass single root flag
  155. )
  156. },
  157. })
  158. const { host } = define({
  159. setup() {
  160. return createComponent(Parent, {
  161. class: () => rootClass.value,
  162. })
  163. },
  164. }).render()
  165. const list = host.children[0].classList
  166. // assert classes without being order-sensitive
  167. function assertClasses(cls: string[]) {
  168. expect(list.length).toBe(cls.length)
  169. for (const c of cls) {
  170. expect(list.contains(c)).toBe(true)
  171. }
  172. }
  173. assertClasses(['root', 'parent', 'child'])
  174. rootClass.value = 'root1'
  175. await nextTick()
  176. assertClasses(['root1', 'parent', 'child'])
  177. parentClass.value = 'parent1'
  178. await nextTick()
  179. assertClasses(['root1', 'parent1', 'child'])
  180. childClass.value = 'child1'
  181. await nextTick()
  182. assertClasses(['root1', 'parent1', 'child1'])
  183. })
  184. it('should merge styles', async () => {
  185. const rootStyle: Ref<string | Record<string, string>> = ref('color:red')
  186. const parentStyle: Ref<string | null> = ref('font-size:12px')
  187. const childStyle = ref('font-weight:bold')
  188. const t0 = template('<div>', true /* root */)
  189. const Child = defineVaporComponent({
  190. setup() {
  191. const n = t0() as Element
  192. renderEffect(() => {
  193. // binding on template root generates incremental class setter
  194. setStyle(n, childStyle.value)
  195. })
  196. return n
  197. },
  198. })
  199. const Parent = defineVaporComponent({
  200. setup() {
  201. return createComponent(
  202. Child,
  203. {
  204. style: () => parentStyle.value,
  205. },
  206. null,
  207. true, // pass single root flag
  208. )
  209. },
  210. })
  211. const { host } = define({
  212. setup() {
  213. return createComponent(Parent, {
  214. style: () => rootStyle.value,
  215. })
  216. },
  217. }).render()
  218. const el = host.children[0] as HTMLElement
  219. function getCSS() {
  220. return el.style.cssText.replace(/\s+/g, '')
  221. }
  222. function assertStyles() {
  223. const css = getCSS()
  224. expect(css).toContain(stringifyStyle(rootStyle.value))
  225. if (parentStyle.value) {
  226. expect(css).toContain(stringifyStyle(parentStyle.value))
  227. }
  228. expect(css).toContain(stringifyStyle(childStyle.value))
  229. }
  230. assertStyles()
  231. rootStyle.value = { color: 'green' }
  232. await nextTick()
  233. assertStyles()
  234. expect(getCSS()).not.toContain('color:red')
  235. parentStyle.value = null
  236. await nextTick()
  237. assertStyles()
  238. expect(getCSS()).not.toContain('font-size:12px')
  239. childStyle.value = 'font-weight:500'
  240. await nextTick()
  241. assertStyles()
  242. expect(getCSS()).not.toContain('font-size:bold')
  243. })
  244. test('parent value should take priority', async () => {
  245. const parentVal = ref('parent')
  246. const childVal = ref('child')
  247. const t0 = template('<div>', true /* root */)
  248. const Child = defineVaporComponent({
  249. setup() {
  250. const n = t0()
  251. renderEffect(() => {
  252. // prop bindings on template root generates extra `root: true` flag
  253. setProp(n, 'id', childVal.value)
  254. setProp(n, 'aria-x', childVal.value)
  255. setDynamicProps(n, [{ 'aria-y': childVal.value }])
  256. })
  257. return n
  258. },
  259. })
  260. const { host } = define({
  261. setup() {
  262. return createComponent(Child, {
  263. id: () => parentVal.value,
  264. 'aria-x': () => parentVal.value,
  265. 'aria-y': () => parentVal.value,
  266. })
  267. },
  268. }).render()
  269. const el = host.children[0]
  270. expect(el.id).toBe(parentVal.value)
  271. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  272. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  273. childVal.value = 'child1'
  274. await nextTick()
  275. expect(el.id).toBe(parentVal.value)
  276. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  277. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  278. parentVal.value = 'parent1'
  279. await nextTick()
  280. expect(el.id).toBe(parentVal.value)
  281. expect(el.getAttribute('aria-x')).toBe(parentVal.value)
  282. expect(el.getAttribute('aria-y')).toBe(parentVal.value)
  283. })
  284. it('empty string should not be passed to classList.add', async () => {
  285. const t0 = template('<div>', true /* root */)
  286. const Child = defineVaporComponent({
  287. setup() {
  288. const n = t0() as Element
  289. renderEffect(() => {
  290. setClass(n, {
  291. foo: false,
  292. })
  293. })
  294. return n
  295. },
  296. })
  297. const Parent = defineVaporComponent({
  298. setup() {
  299. return createComponent(
  300. Child,
  301. {
  302. class: () => ({
  303. bar: false,
  304. }),
  305. },
  306. null,
  307. true,
  308. )
  309. },
  310. })
  311. const { host } = define({
  312. setup() {
  313. return createComponent(Parent)
  314. },
  315. }).render()
  316. const el = host.children[0]
  317. expect(el.classList.length).toBe(0)
  318. })
  319. })