componentProps.spec.ts 13 KB

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