componentProps.spec.ts 12 KB

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