componentProps.spec.ts 12 KB

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