componentProps.spec.ts 13 KB

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