componentProps.spec.ts 13 KB

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