componentAttrs.spec.ts 9.6 KB

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