componentProps.spec.ts 12 KB

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