componentProps.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentProps.spec.ts`.
  2. // NOTE: not supported
  3. // mixins
  4. // caching
  5. import { setCurrentInstance } from '../src/component'
  6. import {
  7. children,
  8. defineComponent,
  9. getCurrentInstance,
  10. nextTick,
  11. ref,
  12. setText,
  13. template,
  14. watchEffect,
  15. } from '../src'
  16. import { makeRender } from './_utils'
  17. const define = makeRender<any>()
  18. describe('component props (vapor)', () => {
  19. test('stateful', () => {
  20. let props: any
  21. let attrs: any
  22. const { render } = define({
  23. props: ['fooBar', 'barBaz'],
  24. render() {
  25. const instance = getCurrentInstance()!
  26. props = instance.props
  27. attrs = instance.attrs
  28. },
  29. })
  30. render({
  31. get fooBar() {
  32. return 1
  33. },
  34. get bar() {
  35. return 2
  36. },
  37. })
  38. expect(props.fooBar).toEqual(1)
  39. expect(attrs.bar).toEqual(2)
  40. // test passing kebab-case and resolving to camelCase
  41. render({
  42. get ['foo-bar']() {
  43. return 2
  44. },
  45. get bar() {
  46. return 3
  47. },
  48. get baz() {
  49. return 4
  50. },
  51. })
  52. expect(props.fooBar).toEqual(2)
  53. expect(attrs.bar).toEqual(3)
  54. expect(attrs.baz).toEqual(4)
  55. // test updating kebab-case should not delete it (#955)
  56. render({
  57. get ['foo-bar']() {
  58. return 3
  59. },
  60. get bar() {
  61. return 3
  62. },
  63. get baz() {
  64. return 4
  65. },
  66. get barBaz() {
  67. return 5
  68. },
  69. })
  70. expect(props.fooBar).toEqual(3)
  71. expect(props.barBaz).toEqual(5)
  72. expect(attrs.bar).toEqual(3)
  73. expect(attrs.baz).toEqual(4)
  74. render({
  75. get qux() {
  76. return 5
  77. },
  78. })
  79. expect(props.fooBar).toBeUndefined()
  80. expect(props.barBaz).toBeUndefined()
  81. expect(attrs.qux).toEqual(5)
  82. })
  83. test.todo('stateful with setup', () => {
  84. // TODO:
  85. })
  86. test('functional with declaration', () => {
  87. let props: any
  88. let attrs: any
  89. const { component: Comp, render } = define((_props: any) => {
  90. const instance = getCurrentInstance()!
  91. props = instance.props
  92. attrs = instance.attrs
  93. return {}
  94. })
  95. Comp.props = ['foo']
  96. render({
  97. get foo() {
  98. return 1
  99. },
  100. get bar() {
  101. return 2
  102. },
  103. })
  104. expect(props.foo).toEqual(1)
  105. expect(attrs.bar).toEqual(2)
  106. render({
  107. get foo() {
  108. return 2
  109. },
  110. get bar() {
  111. return 3
  112. },
  113. get baz() {
  114. return 4
  115. },
  116. })
  117. expect(props.foo).toEqual(2)
  118. expect(attrs.bar).toEqual(3)
  119. expect(attrs.baz).toEqual(4)
  120. render({
  121. get qux() {
  122. return 5
  123. },
  124. })
  125. expect(props.foo).toBeUndefined()
  126. expect(attrs.qux).toEqual(5)
  127. })
  128. // FIXME:
  129. test('functional without declaration', () => {
  130. let props: any
  131. let attrs: any
  132. const { render } = define((_props: any, { attrs: _attrs }: any) => {
  133. const instance = getCurrentInstance()!
  134. props = instance.props
  135. attrs = instance.attrs
  136. return {}
  137. })
  138. render({
  139. get foo() {
  140. return 1
  141. },
  142. })
  143. expect(props.foo).toEqual(1)
  144. expect(attrs.foo).toEqual(1)
  145. render({
  146. get foo() {
  147. return 2
  148. },
  149. })
  150. expect(props.foo).toEqual(2)
  151. expect(attrs.foo).toEqual(2)
  152. })
  153. test('boolean casting', () => {
  154. let props: any
  155. const { render } = define({
  156. props: {
  157. foo: Boolean,
  158. bar: Boolean,
  159. baz: Boolean,
  160. qux: Boolean,
  161. },
  162. render() {
  163. const instance = getCurrentInstance()!
  164. props = instance.props
  165. },
  166. })
  167. render({
  168. // absent should cast to false
  169. bar: '', // empty string should cast to true
  170. baz: 'baz', // same string should cast to true
  171. qux: 'ok', // other values should be left in-tact (but raise warning)
  172. })
  173. expect(props.foo).toBe(false)
  174. expect(props.bar).toBe(true)
  175. expect(props.baz).toBe(true)
  176. expect(props.qux).toBe('ok')
  177. })
  178. test('default value', () => {
  179. let props: any
  180. const defaultFn = vi.fn(() => ({ a: 1 }))
  181. const defaultBaz = vi.fn(() => ({ b: 1 }))
  182. const { render } = define({
  183. props: {
  184. foo: {
  185. default: 1,
  186. },
  187. bar: {
  188. default: defaultFn,
  189. },
  190. baz: {
  191. type: Function,
  192. default: defaultBaz,
  193. },
  194. },
  195. render() {
  196. const instance = getCurrentInstance()!
  197. props = instance.props
  198. },
  199. })
  200. render({
  201. get foo() {
  202. return 2
  203. },
  204. })
  205. expect(props.foo).toBe(2)
  206. // const prevBar = props.bar
  207. props.bar
  208. expect(props.bar).toEqual({ a: 1 })
  209. expect(props.baz).toEqual(defaultBaz)
  210. // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: (caching is not supported)
  211. expect(defaultFn).toHaveBeenCalledTimes(3)
  212. expect(defaultBaz).toHaveBeenCalledTimes(0)
  213. // #999: updates should not cause default factory of unchanged prop to be
  214. // called again
  215. render({
  216. get foo() {
  217. return 3
  218. },
  219. })
  220. expect(props.foo).toBe(3)
  221. expect(props.bar).toEqual({ a: 1 })
  222. // expect(props.bar).toBe(prevBar) // failed: (caching is not supported)
  223. // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
  224. render({
  225. get bar() {
  226. return { b: 2 }
  227. },
  228. })
  229. expect(props.foo).toBe(1)
  230. expect(props.bar).toEqual({ b: 2 })
  231. // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
  232. render({
  233. get foo() {
  234. return 3
  235. },
  236. get bar() {
  237. return { b: 3 }
  238. },
  239. })
  240. expect(props.foo).toBe(3)
  241. expect(props.bar).toEqual({ b: 3 })
  242. // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
  243. render({
  244. get bar() {
  245. return { b: 4 }
  246. },
  247. })
  248. expect(props.foo).toBe(1)
  249. expect(props.bar).toEqual({ b: 4 })
  250. // expect(defaultFn).toHaveBeenCalledTimes(1) // failed: caching is not supported (called 3 times)
  251. })
  252. test.todo('using inject in default value factory', () => {
  253. // TODO: impl inject
  254. })
  255. // NOTE: maybe it's unnecessary
  256. // https://github.com/vuejs/core-vapor/pull/99#discussion_r1472647377
  257. test('optimized props updates', async () => {
  258. const renderChild = define({
  259. props: ['foo'],
  260. render() {
  261. const instance = getCurrentInstance()!
  262. const t0 = template('<div><!></div>')
  263. const n0 = t0()
  264. const n1 = children(n0, 0)
  265. watchEffect(() => {
  266. setText(n1, instance.props.foo)
  267. })
  268. return n0
  269. },
  270. }).render
  271. const foo = ref(1)
  272. const id = ref('a')
  273. const { instance, host } = define({
  274. setup() {
  275. return { foo, id }
  276. },
  277. render(_ctx: Record<string, any>) {
  278. const t0 = template('')
  279. const n0 = t0()
  280. renderChild(
  281. {
  282. get foo() {
  283. return _ctx.foo
  284. },
  285. get id() {
  286. return _ctx.id
  287. },
  288. },
  289. n0 as any, // TODO: type
  290. )
  291. return n0
  292. },
  293. }).render()
  294. const reset = setCurrentInstance(instance)
  295. // expect(host.innerHTML).toBe('<div id="a">1</div>') // TODO: Fallthrough Attributes
  296. expect(host.innerHTML).toBe('<div>1</div>')
  297. foo.value++
  298. await nextTick()
  299. // expect(host.innerHTML).toBe('<div id="a">2</div>') // TODO: Fallthrough Attributes
  300. expect(host.innerHTML).toBe('<div>2</div>')
  301. // id.value = 'b'
  302. // await nextTick()
  303. // expect(host.innerHTML).toBe('<div id="b">2</div>') // TODO: Fallthrough Attributes
  304. reset()
  305. })
  306. describe('validator', () => {
  307. test('validator should be called with two arguments', () => {
  308. let args: any
  309. const mockFn = vi.fn((..._args: any[]) => {
  310. args = _args
  311. return true
  312. })
  313. const props = {
  314. get foo() {
  315. return 1
  316. },
  317. get bar() {
  318. return 2
  319. },
  320. }
  321. define({
  322. props: {
  323. foo: {
  324. type: Number,
  325. validator: (value: any, props: any) => mockFn(value, props),
  326. },
  327. bar: {
  328. type: Number,
  329. },
  330. },
  331. render() {
  332. const t0 = template('<div/>')
  333. const n0 = t0()
  334. return n0
  335. },
  336. }).render(props)
  337. expect(mockFn).toHaveBeenCalled()
  338. // NOTE: Vapor Component props defined by getter. So, `props` not Equal to `{ foo: 1, bar: 2 }`
  339. // expect(mockFn).toHaveBeenCalledWith(1, { foo: 1, bar: 2 })
  340. expect(args.length).toBe(2)
  341. expect(args[0]).toBe(1)
  342. expect(args[1].foo).toEqual(1)
  343. expect(args[1].bar).toEqual(2)
  344. })
  345. // TODO: impl setter and warnner
  346. test.todo(
  347. 'validator should not be able to mutate other props',
  348. async () => {
  349. const mockFn = vi.fn((...args: any[]) => true)
  350. defineComponent({
  351. props: {
  352. foo: {
  353. type: Number,
  354. validator: (value: any, props: any) => !!(props.bar = 1),
  355. },
  356. bar: {
  357. type: Number,
  358. validator: (value: any) => mockFn(value),
  359. },
  360. },
  361. render() {
  362. const t0 = template('<div/>')
  363. const n0 = t0()
  364. return n0
  365. },
  366. }).render({
  367. get foo() {
  368. return 1
  369. },
  370. get bar() {
  371. return 2
  372. },
  373. })
  374. expect(
  375. `Set operation on key "bar" failed: target is readonly.`,
  376. ).toHaveBeenWarnedLast()
  377. expect(mockFn).toHaveBeenCalledWith(2)
  378. },
  379. )
  380. })
  381. test.todo('warn props mutation', () => {
  382. // TODO: impl warn
  383. })
  384. test('warn absent required props', () => {
  385. define({
  386. props: {
  387. bool: { type: Boolean, required: true },
  388. str: { type: String, required: true },
  389. num: { type: Number, required: true },
  390. },
  391. setup() {
  392. return () => null
  393. },
  394. }).render()
  395. expect(`Missing required prop: "bool"`).toHaveBeenWarned()
  396. expect(`Missing required prop: "str"`).toHaveBeenWarned()
  397. expect(`Missing required prop: "num"`).toHaveBeenWarned()
  398. })
  399. // NOTE: type check is not supported in vapor
  400. // test('warn on type mismatch', () => {})
  401. // #3495
  402. test('should not warn required props using kebab-case', async () => {
  403. define({
  404. props: {
  405. fooBar: { type: String, required: true },
  406. },
  407. setup() {
  408. return () => null
  409. },
  410. }).render({
  411. get ['foo-bar']() {
  412. return 'hello'
  413. },
  414. })
  415. expect(`Missing required prop: "fooBar"`).not.toHaveBeenWarned()
  416. })
  417. test('props type support BigInt', () => {
  418. const { host } = define({
  419. props: {
  420. foo: BigInt,
  421. },
  422. render() {
  423. const instance = getCurrentInstance()!
  424. const t0 = template('<div></div>')
  425. const n0 = t0()
  426. const n1 = children(n0, 0)
  427. watchEffect(() => {
  428. setText(n1, instance.props.foo)
  429. })
  430. return n0
  431. },
  432. }).render({
  433. get foo() {
  434. return BigInt(BigInt(100000111)) + BigInt(2000000000) * BigInt(30000000)
  435. },
  436. })
  437. expect(host.innerHTML).toBe('<div>60000000100000111</div>')
  438. })
  439. // #3288
  440. test.todo(
  441. 'declared prop key should be present even if not passed',
  442. async () => {
  443. // let initialKeys: string[] = []
  444. // const changeSpy = vi.fn()
  445. // const passFoo = ref(false)
  446. // const Comp = {
  447. // props: ['foo'],
  448. // setup() {
  449. // const instance = getCurrentInstance()!
  450. // initialKeys = Object.keys(instance.props)
  451. // watchEffect(changeSpy)
  452. // return {}
  453. // },
  454. // render() {
  455. // return {}
  456. // },
  457. // }
  458. // const Parent = createIf(
  459. // () => passFoo.value,
  460. // () => {
  461. // return render(Comp , { foo: 1 }, host) // TODO: createComponent fn
  462. // },
  463. // )
  464. // // expect(changeSpy).toHaveBeenCalledTimes(1)
  465. },
  466. )
  467. // #3371
  468. test.todo(`avoid double-setting props when casting`, async () => {
  469. // TODO: proide, slots
  470. })
  471. test('support null in required + multiple-type declarations', () => {
  472. const { render } = define({
  473. props: {
  474. foo: { type: [Function, null], required: true },
  475. },
  476. render() {},
  477. })
  478. expect(() => {
  479. render({ foo: () => {} })
  480. }).not.toThrow()
  481. expect(() => {
  482. render({ foo: null })
  483. }).not.toThrow()
  484. })
  485. // #5016
  486. test('handling attr with undefined value', () => {
  487. const { render, host } = define({
  488. render() {
  489. const instance = getCurrentInstance()!
  490. const t0 = template('<div></div>')
  491. const n0 = t0()
  492. const n1 = children(n0, 0)
  493. watchEffect(() => {
  494. setText(
  495. n1,
  496. JSON.stringify(instance.attrs) + Object.keys(instance.attrs),
  497. )
  498. })
  499. return n0
  500. },
  501. })
  502. let attrs: any = {
  503. get foo() {
  504. return undefined
  505. },
  506. }
  507. render(attrs)
  508. expect(host.innerHTML).toBe(
  509. `<div>${JSON.stringify(attrs) + Object.keys(attrs)}</div>`,
  510. )
  511. })
  512. // #6915
  513. test('should not mutate original props long-form definition object', () => {
  514. const props = {
  515. msg: {
  516. type: String,
  517. },
  518. }
  519. define({ props, render() {} }).render({ msg: 'test' })
  520. expect(Object.keys(props.msg).length).toBe(1)
  521. })
  522. })