componentProps.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
  2. import {
  3. // currentInstance,
  4. inject,
  5. nextTick,
  6. provide,
  7. ref,
  8. toRefs,
  9. watch,
  10. } from '@vue/runtime-dom'
  11. import {
  12. createComponent,
  13. defineVaporComponent,
  14. renderEffect,
  15. template,
  16. } from '../src'
  17. import { makeRender } from './_utils'
  18. import { setElementText } from '../src/dom/prop'
  19. const define = makeRender<any>()
  20. describe('component: props', () => {
  21. test('stateful', () => {
  22. let props: any
  23. let attrs: any
  24. const { render } = define({
  25. props: ['fooBar', 'barBaz'],
  26. setup(_props: any, { attrs: _attrs }: any) {
  27. props = _props
  28. attrs = _attrs
  29. return []
  30. },
  31. })
  32. render({ fooBar: () => 1, bar: () => 2 })
  33. expect(props).toEqual({ fooBar: 1 })
  34. expect(attrs).toEqual({ bar: 2 })
  35. // test passing kebab-case and resolving to camelCase
  36. render({ 'foo-bar': () => 2, bar: () => 3, baz: () => 4 })
  37. expect(props).toEqual({ fooBar: 2 })
  38. expect(attrs).toEqual({ bar: 3, baz: 4 })
  39. // test updating kebab-case should not delete it (#955)
  40. render({ 'foo-bar': () => 3, bar: () => 3, baz: () => 4, barBaz: () => 5 })
  41. expect(props).toEqual({ fooBar: 3, barBaz: 5 })
  42. expect(attrs).toEqual({ bar: 3, baz: 4 })
  43. // remove the props with camelCase key (#1412)
  44. render({ qux: () => 5 })
  45. expect(props).toEqual({})
  46. expect(attrs).toEqual({ qux: 5 })
  47. })
  48. test('stateful with setup', () => {
  49. let props: any
  50. let attrs: any
  51. const { render } = define({
  52. props: ['foo'],
  53. setup(_props: any, { attrs: _attrs }: any) {
  54. props = _props
  55. attrs = _attrs
  56. return []
  57. },
  58. })
  59. render({ foo: () => 1, bar: () => 2 })
  60. expect(props).toEqual({ foo: 1 })
  61. expect(attrs).toEqual({ bar: 2 })
  62. render({ foo: () => 2, bar: () => 3, baz: () => 4 })
  63. expect(props).toEqual({ foo: 2 })
  64. expect(attrs).toEqual({ bar: 3, baz: 4 })
  65. render({ qux: () => 5 })
  66. expect(props).toEqual({})
  67. expect(attrs).toEqual({ qux: 5 })
  68. })
  69. test('functional with declaration', () => {
  70. let props: any
  71. let attrs: any
  72. const { component: Comp, render } = define(
  73. (_props: any, { attrs: _attrs }: any) => {
  74. props = _props
  75. attrs = _attrs
  76. return []
  77. },
  78. )
  79. Comp.props = ['foo']
  80. render({ foo: () => 1, bar: () => 2 })
  81. expect(props).toEqual({ foo: 1 })
  82. expect(attrs).toEqual({ bar: 2 })
  83. render({ foo: () => 2, bar: () => 3, baz: () => 4 })
  84. expect(props).toEqual({ foo: 2 })
  85. expect(attrs).toEqual({ bar: 3, baz: 4 })
  86. render({ qux: () => 5 })
  87. expect(props).toEqual({})
  88. expect(attrs).toEqual({ qux: 5 })
  89. })
  90. test('functional without declaration', () => {
  91. let props: any
  92. let attrs: any
  93. const { render } = define((_props: any, { attrs: _attrs }: any) => {
  94. props = _props
  95. attrs = _attrs
  96. return []
  97. })
  98. render({ foo: () => 1 })
  99. expect(props).toEqual({ foo: 1 })
  100. expect(attrs).toEqual({ foo: 1 })
  101. expect(props).toBe(attrs)
  102. render({ bar: () => 2 })
  103. expect(props).toEqual({ bar: 2 })
  104. expect(attrs).toEqual({ bar: 2 })
  105. expect(props).toBe(attrs)
  106. })
  107. test('functional defineVaporComponent without declaration', () => {
  108. let props: any
  109. let attrs: any
  110. const { render } = define(
  111. defineVaporComponent((_props: any, { attrs: _attrs }: any) => {
  112. props = _props
  113. attrs = _attrs
  114. return []
  115. }),
  116. )
  117. render({ foo: () => 1 })
  118. expect(props).toEqual({})
  119. expect(attrs).toEqual({ foo: 1 })
  120. render({ bar: () => 2 })
  121. expect(props).toEqual({})
  122. expect(attrs).toEqual({ bar: 2 })
  123. })
  124. test('boolean casting', () => {
  125. let props: any
  126. const { render } = define({
  127. props: {
  128. foo: Boolean,
  129. bar: Boolean,
  130. baz: Boolean,
  131. qux: Boolean,
  132. },
  133. setup(_props: any) {
  134. props = _props
  135. return []
  136. },
  137. })
  138. render({
  139. // absent should cast to false
  140. bar: () => '', // empty string should cast to true
  141. baz: () => 'baz', // same string should cast to true
  142. qux: () => 'ok', // other values should be left in-tact (but raise warning)
  143. })
  144. expect(props.foo).toBe(false)
  145. expect(props.bar).toBe(true)
  146. expect(props.baz).toBe(true)
  147. expect(props.qux).toBe('ok')
  148. expect('type check failed for prop "qux"').toHaveBeenWarned()
  149. })
  150. test('default value', () => {
  151. let props: any
  152. const defaultFn = vi.fn(() => ({ a: 1 }))
  153. const defaultBaz = vi.fn(() => ({ b: 1 }))
  154. const { render } = define({
  155. props: {
  156. foo: {
  157. default: 1,
  158. },
  159. bar: {
  160. default: defaultFn,
  161. },
  162. baz: {
  163. type: Function,
  164. default: defaultBaz,
  165. },
  166. },
  167. setup(_props: any) {
  168. props = _props
  169. return []
  170. },
  171. })
  172. render({ foo: () => 2 })
  173. expect(props.foo).toBe(2)
  174. expect(props.bar).toEqual({ a: 1 })
  175. expect(props.baz).toEqual(defaultBaz)
  176. expect(defaultFn).toHaveBeenCalledTimes(1)
  177. expect(defaultBaz).toHaveBeenCalledTimes(0)
  178. // #999: updates should not cause default factory of unchanged prop to be
  179. // called again
  180. render({ foo: () => 3 })
  181. expect(props.foo).toBe(3)
  182. expect(props.bar).toEqual({ a: 1 })
  183. render({ bar: () => ({ b: 2 }) })
  184. expect(props.foo).toBe(1)
  185. expect(props.bar).toEqual({ b: 2 })
  186. render({
  187. foo: () => 3,
  188. bar: () => ({ b: 3 }),
  189. })
  190. expect(props.foo).toBe(3)
  191. expect(props.bar).toEqual({ b: 3 })
  192. render({ bar: () => ({ b: 4 }) })
  193. expect(props.foo).toBe(1)
  194. expect(props.bar).toEqual({ b: 4 })
  195. })
  196. test('using inject in default value factory', () => {
  197. let props: any
  198. const Child = defineVaporComponent({
  199. props: {
  200. test: {
  201. default: () => inject('test', 'default'),
  202. },
  203. },
  204. setup(_props) {
  205. props = _props
  206. return []
  207. },
  208. })
  209. const { render } = define({
  210. setup() {
  211. provide('test', 'injected')
  212. return createComponent(Child)
  213. },
  214. })
  215. render()
  216. expect(props.test).toBe('injected')
  217. })
  218. test('optimized props updates', async () => {
  219. const t0 = template('<div>')
  220. const { component: Child } = define({
  221. props: ['foo'],
  222. setup(props: any) {
  223. const n0 = t0()
  224. renderEffect(() => setElementText(n0, props.foo))
  225. return n0
  226. },
  227. })
  228. const foo = ref(1)
  229. const id = ref('a')
  230. const { host } = define({
  231. setup() {
  232. return { foo, id }
  233. },
  234. render(_ctx: Record<string, any>) {
  235. return createComponent(
  236. Child,
  237. {
  238. foo: () => _ctx.foo,
  239. id: () => _ctx.id,
  240. },
  241. null,
  242. true,
  243. )
  244. },
  245. }).render()
  246. expect(host.innerHTML).toBe('<div id="a">1</div>')
  247. foo.value++
  248. await nextTick()
  249. expect(host.innerHTML).toBe('<div id="a">2</div>')
  250. id.value = 'b'
  251. await nextTick()
  252. expect(host.innerHTML).toBe('<div id="b">2</div>')
  253. })
  254. describe('validator', () => {
  255. test('validator should be called with two arguments', () => {
  256. const mockFn = vi.fn((...args: any[]) => true)
  257. const props = {
  258. foo: () => 1,
  259. bar: () => 2,
  260. }
  261. const t0 = template('<div/>')
  262. define({
  263. props: {
  264. foo: {
  265. type: Number,
  266. validator: (value: any, props: any) => mockFn(value, props),
  267. },
  268. bar: {
  269. type: Number,
  270. },
  271. },
  272. setup() {
  273. return t0()
  274. },
  275. }).render(props)
  276. expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
  277. })
  278. test('validator should not be able to mutate other props', async () => {
  279. const mockFn = vi.fn((...args: any[]) => true)
  280. define({
  281. props: {
  282. foo: {
  283. type: Number,
  284. validator: (value: any, props: any) => !!(props.bar = 1),
  285. },
  286. bar: {
  287. type: Number,
  288. validator: (value: any) => mockFn(value),
  289. },
  290. },
  291. setup() {
  292. const t0 = template('<div/>')
  293. const n0 = t0()
  294. return n0
  295. },
  296. }).render!({
  297. foo() {
  298. return 1
  299. },
  300. bar() {
  301. return 2
  302. },
  303. })
  304. expect(
  305. `Set operation on key "bar" failed: target is readonly.`,
  306. ).toHaveBeenWarnedLast()
  307. expect(mockFn).toHaveBeenCalledWith(2)
  308. })
  309. })
  310. test('warn props mutation', () => {
  311. let props: any
  312. const { render } = define({
  313. props: ['foo'],
  314. setup(_props: any) {
  315. props = _props
  316. return []
  317. },
  318. })
  319. render({ foo: () => 1 })
  320. expect(props.foo).toBe(1)
  321. props.foo = 2
  322. expect(`Attempt to mutate prop "foo" failed`).toHaveBeenWarned()
  323. })
  324. test('warn absent required props', () => {
  325. define({
  326. props: {
  327. bool: { type: Boolean, required: true },
  328. str: { type: String, required: true },
  329. num: { type: Number, required: true },
  330. },
  331. setup() {
  332. return []
  333. },
  334. }).render()
  335. expect(`Missing required prop: "bool"`).toHaveBeenWarned()
  336. expect(`Missing required prop: "str"`).toHaveBeenWarned()
  337. expect(`Missing required prop: "num"`).toHaveBeenWarned()
  338. })
  339. // NOTE: type check is not supported in vapor
  340. // test('warn on type mismatch', () => {})
  341. // #3495
  342. test('should not warn required props using kebab-case', async () => {
  343. define({
  344. props: {
  345. fooBar: { type: String, required: true },
  346. },
  347. setup() {
  348. return []
  349. },
  350. }).render({
  351. ['foo-bar']: () => 'hello',
  352. })
  353. expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
  354. })
  355. test('props type support BigInt', () => {
  356. const t0 = template('<div>')
  357. const { host } = define({
  358. props: {
  359. foo: BigInt,
  360. },
  361. setup(props: any) {
  362. const n0 = t0()
  363. renderEffect(() => setElementText(n0, props.foo))
  364. return n0
  365. },
  366. }).render({
  367. foo: () =>
  368. BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000),
  369. })
  370. expect(host.innerHTML).toBe('<div>60000000100000111</div>')
  371. })
  372. // #3474
  373. test('should cache the value returned from the default factory to avoid unnecessary watcher trigger', async () => {
  374. let count = 0
  375. const { render, html } = define({
  376. props: {
  377. foo: {
  378. type: Object,
  379. default: () => ({ val: 1 }),
  380. },
  381. bar: Number,
  382. },
  383. setup(props: any) {
  384. watch(
  385. () => props.foo,
  386. () => {
  387. count++
  388. },
  389. )
  390. const t0 = template('<h1></h1>')
  391. const n0 = t0()
  392. renderEffect(() => {
  393. setElementText(n0, String(props.foo.val) + String(props.bar))
  394. })
  395. return n0
  396. },
  397. })
  398. const foo = ref()
  399. const bar = ref(0)
  400. render({ foo: () => foo.value, bar: () => bar.value })
  401. expect(html()).toBe(`<h1>10</h1>`)
  402. expect(count).toBe(0)
  403. bar.value++
  404. await nextTick()
  405. expect(html()).toBe(`<h1>11</h1>`)
  406. expect(count).toBe(0)
  407. })
  408. // #3288
  409. test('declared prop key should be present even if not passed', async () => {
  410. let initialKeys: string[] = []
  411. const changeSpy = vi.fn()
  412. const passFoo = ref(false)
  413. const Comp: any = {
  414. props: {
  415. foo: String,
  416. },
  417. setup(props: any) {
  418. initialKeys = Object.keys(props)
  419. const { foo } = toRefs(props)
  420. watch(foo, changeSpy)
  421. return []
  422. },
  423. }
  424. define(() =>
  425. createComponent(Comp, {
  426. $: [() => (passFoo.value ? { foo: 'ok' } : {})],
  427. }),
  428. ).render()
  429. expect(initialKeys).toMatchObject(['foo'])
  430. passFoo.value = true
  431. await nextTick()
  432. expect(changeSpy).toHaveBeenCalledTimes(1)
  433. })
  434. test('should not warn invalid watch source when directly watching props', async () => {
  435. const changeSpy = vi.fn()
  436. const { render, html } = define({
  437. props: {
  438. foo: {
  439. type: String,
  440. },
  441. },
  442. setup(props: any) {
  443. watch(props, changeSpy)
  444. const t0 = template('<h1></h1>')
  445. const n0 = t0()
  446. renderEffect(() => {
  447. setElementText(n0, String(props.foo))
  448. })
  449. return n0
  450. },
  451. })
  452. const foo = ref('foo')
  453. render({ foo: () => foo.value })
  454. expect(html()).toBe(`<h1>foo</h1>`)
  455. expect('Invalid watch source').not.toHaveBeenWarned()
  456. foo.value = 'bar'
  457. await nextTick()
  458. expect(html()).toBe(`<h1>bar</h1>`)
  459. expect(changeSpy).toHaveBeenCalledTimes(1)
  460. })
  461. test('support null in required + multiple-type declarations', () => {
  462. const { render } = define({
  463. props: {
  464. foo: { type: [Function, null], required: true },
  465. },
  466. setup() {
  467. return []
  468. },
  469. })
  470. expect(() => {
  471. render({ foo: () => () => {} })
  472. }).not.toThrow()
  473. expect(() => {
  474. render({ foo: () => null })
  475. }).not.toThrow()
  476. })
  477. // #5016
  478. test('handling attr with undefined value', () => {
  479. const { render, host } = define({
  480. inheritAttrs: false,
  481. setup(_: any, { attrs }: any) {
  482. const t0 = template('<div></div>')
  483. const n0 = t0()
  484. renderEffect(() =>
  485. setElementText(n0, JSON.stringify(attrs) + Object.keys(attrs)),
  486. )
  487. return n0
  488. },
  489. })
  490. const attrs: any = { foo: () => undefined }
  491. render(attrs)
  492. expect(host.innerHTML).toBe(
  493. `<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
  494. )
  495. })
  496. // #6915
  497. test('should not mutate original props long-form definition object', () => {
  498. const props = {
  499. msg: {
  500. type: String,
  501. },
  502. }
  503. define({ props, setup: () => [] }).render({ msg: () => 'test' })
  504. expect(Object.keys(props.msg).length).toBe(1)
  505. })
  506. test('should warn against reserved prop names', () => {
  507. const { render } = define({
  508. props: {
  509. $foo: String,
  510. },
  511. setup: () => [],
  512. })
  513. render({ msg: () => 'test' })
  514. expect(`Invalid prop name: "$foo"`).toHaveBeenWarned()
  515. })
  516. describe('dynamic props source caching', () => {
  517. test('v-bind object should be cached when child accesses multiple props', () => {
  518. let sourceCallCount = 0
  519. const obj = ref({ foo: 1, bar: 2, baz: 3 })
  520. const t0 = template('<div></div>')
  521. const Child = defineVaporComponent({
  522. props: ['foo', 'bar', 'baz'],
  523. setup(props: any) {
  524. const n0 = t0()
  525. // Child component accesses multiple props
  526. renderEffect(() => {
  527. setElementText(n0, `${props.foo}-${props.bar}-${props.baz}`)
  528. })
  529. return n0
  530. },
  531. })
  532. const { host } = define({
  533. setup() {
  534. return createComponent(Child, {
  535. $: [
  536. () => {
  537. sourceCallCount++
  538. return obj.value
  539. },
  540. ],
  541. })
  542. },
  543. }).render()
  544. expect(host.innerHTML).toBe('<div>1-2-3</div>')
  545. // Source should only be called once even though 3 props are accessed
  546. expect(sourceCallCount).toBe(1)
  547. })
  548. test('v-bind object should update when source changes', async () => {
  549. let sourceCallCount = 0
  550. const obj = ref({ foo: 1, bar: 2 })
  551. const t0 = template('<div></div>')
  552. const Child = defineVaporComponent({
  553. props: ['foo', 'bar'],
  554. setup(props: any) {
  555. const n0 = t0()
  556. renderEffect(() => {
  557. setElementText(n0, `${props.foo}-${props.bar}`)
  558. })
  559. return n0
  560. },
  561. })
  562. const { host } = define({
  563. setup() {
  564. return createComponent(Child, {
  565. $: [
  566. () => {
  567. sourceCallCount++
  568. return obj.value
  569. },
  570. ],
  571. })
  572. },
  573. }).render()
  574. expect(host.innerHTML).toBe('<div>1-2</div>')
  575. expect(sourceCallCount).toBe(1)
  576. // Update source
  577. obj.value = { foo: 10, bar: 20 }
  578. await nextTick()
  579. expect(host.innerHTML).toBe('<div>10-20</div>')
  580. // Should be called again after source changes
  581. expect(sourceCallCount).toBe(2)
  582. })
  583. test('v-bind object should be cached when child accesses multiple attrs', () => {
  584. let sourceCallCount = 0
  585. const obj = ref({ foo: 1, bar: 2, baz: 3 })
  586. const t0 = template('<div></div>')
  587. const Child = defineVaporComponent({
  588. // No props declaration - all become attrs
  589. setup(_: any, { attrs }: any) {
  590. const n0 = t0()
  591. renderEffect(() => {
  592. setElementText(n0, `${attrs.foo}-${attrs.bar}-${attrs.baz}`)
  593. })
  594. return n0
  595. },
  596. })
  597. const { host } = define({
  598. setup() {
  599. return createComponent(Child, {
  600. $: [
  601. () => {
  602. sourceCallCount++
  603. return obj.value
  604. },
  605. ],
  606. })
  607. },
  608. }).render()
  609. expect(host.innerHTML).toBe('<div foo="1" bar="2" baz="3">1-2-3</div>')
  610. // Source should only be called once
  611. expect(sourceCallCount).toBe(1)
  612. })
  613. test('mixed static and dynamic props', async () => {
  614. let sourceCallCount = 0
  615. const obj = ref({ foo: 1 })
  616. const t0 = template('<div></div>')
  617. const Child = defineVaporComponent({
  618. props: ['id', 'foo', 'class'],
  619. setup(props: any) {
  620. const n0 = t0()
  621. renderEffect(() => {
  622. setElementText(n0, `${props.id}-${props.foo}-${props.class}`)
  623. })
  624. return n0
  625. },
  626. })
  627. const { host } = define({
  628. setup() {
  629. return createComponent(Child, {
  630. id: () => 'static',
  631. $: [
  632. () => {
  633. sourceCallCount++
  634. return obj.value
  635. },
  636. { class: () => 'bar' },
  637. ],
  638. })
  639. },
  640. }).render()
  641. expect(host.innerHTML).toBe('<div>static-1-bar</div>')
  642. expect(sourceCallCount).toBe(1)
  643. obj.value = { foo: 2 }
  644. await nextTick()
  645. expect(host.innerHTML).toBe('<div>static-2-bar</div>')
  646. expect(sourceCallCount).toBe(2)
  647. })
  648. })
  649. })