props.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /* eslint-disable @typescript-eslint/ban-types */
  2. import { defineComponent } from 'vue'
  3. import { mount } from '@vue/test-utils'
  4. import { describe, expect, it, vi } from 'vitest'
  5. import { expectTypeOf } from 'expect-type'
  6. import { buildProp, buildProps, definePropType, keysOf, mutable } from '../..'
  7. import type {
  8. EpProp,
  9. EpPropInputDefault,
  10. EpPropMergeType,
  11. IfNever,
  12. ResolvePropType,
  13. UnknownToNever,
  14. Writable,
  15. WritableArray,
  16. epPropKey,
  17. } from '../..'
  18. import type { ExtractPropTypes, PropType } from 'vue'
  19. describe('Types', () => {
  20. it('Writable', () => {
  21. expectTypeOf<Writable<readonly [1, 2, 3]>>().toEqualTypeOf<[1, 2, 3]>()
  22. expectTypeOf<Writable<Readonly<{ a: 'b' }>>>().toEqualTypeOf<{
  23. a: 'b'
  24. }>()
  25. expectTypeOf<Writable<123>>().toEqualTypeOf<123>()
  26. expectTypeOf<
  27. Writable<StringConstructor>
  28. >().not.toEqualTypeOf<StringConstructor>()
  29. })
  30. it('WritableArray', () => {
  31. expectTypeOf<WritableArray<readonly [1, 2, 3]>>().toEqualTypeOf<[1, 2, 3]>()
  32. expectTypeOf<
  33. WritableArray<BooleanConstructor>
  34. >().toEqualTypeOf<BooleanConstructor>()
  35. })
  36. it('IfNever', () => {
  37. expectTypeOf<IfNever<boolean | 123 | '1'>>().toEqualTypeOf<false>()
  38. expectTypeOf<IfNever<never>>().toEqualTypeOf<true>()
  39. })
  40. it('UnknownToNever', () => {
  41. expectTypeOf<UnknownToNever<unknown>>().toEqualTypeOf<never>()
  42. expectTypeOf<UnknownToNever<unknown | 1>>().toEqualTypeOf<never>()
  43. expectTypeOf<UnknownToNever<1>>().toEqualTypeOf<1>()
  44. })
  45. it('ResolvePropType', () => {
  46. expectTypeOf<ResolvePropType<BooleanConstructor>>().toEqualTypeOf<boolean>()
  47. expectTypeOf<ResolvePropType<StringConstructor>>().toEqualTypeOf<string>()
  48. expectTypeOf<ResolvePropType<DateConstructor>>().toEqualTypeOf<Date>()
  49. expectTypeOf<
  50. ResolvePropType<[DateConstructor, NumberConstructor]>
  51. >().toEqualTypeOf<Date | number>()
  52. expectTypeOf<
  53. ResolvePropType<readonly [DateConstructor, NumberConstructor]>
  54. >().toEqualTypeOf<Date | number>()
  55. expectTypeOf<
  56. ResolvePropType<PropType<string | 12 | false>>
  57. >().toEqualTypeOf<string | 12 | false>()
  58. expectTypeOf<ResolvePropType<never>>().toBeNever()
  59. })
  60. it('EpPropMergeType', () => {
  61. expectTypeOf<
  62. EpPropMergeType<StringConstructor | NumberConstructor, 'str', 1>
  63. >().toEqualTypeOf<'str' | 1>()
  64. expectTypeOf<EpPropMergeType<NumberConstructor, 2 | 3, 4>>().toEqualTypeOf<
  65. 2 | 3 | 4
  66. >()
  67. })
  68. it('EpPropInputDefault', () => {
  69. expectTypeOf<EpPropInputDefault<true, 1>>().toBeNever()
  70. expectTypeOf<EpPropInputDefault<false, 1>>().toEqualTypeOf<1 | (() => 1)>()
  71. })
  72. it('EpProp', () => {
  73. expectTypeOf<EpProp<'1', '2', false>>().toEqualTypeOf<{
  74. readonly type: PropType<'1'>
  75. readonly required: false
  76. readonly validator: ((val: unknown) => boolean) | undefined
  77. readonly default: '2'
  78. [epPropKey]: true
  79. }>()
  80. expectTypeOf<EpProp<'1', '2', true>>().toEqualTypeOf<{
  81. readonly type: PropType<'1'>
  82. readonly required: true
  83. readonly validator: ((val: unknown) => boolean) | undefined
  84. readonly default: '2'
  85. [epPropKey]: true
  86. }>()
  87. })
  88. })
  89. describe('buildProp', () => {
  90. it('Only type', () => {
  91. expectTypeOf(
  92. buildProp({
  93. type: definePropType<'a' | 'b'>(String),
  94. } as const)
  95. ).toEqualTypeOf<{
  96. readonly type: PropType<'a' | 'b'>
  97. readonly required: false
  98. readonly validator: ((val: unknown) => boolean) | undefined
  99. [epPropKey]: true
  100. }>()
  101. })
  102. it('Only values', () => {
  103. expectTypeOf(
  104. buildProp({
  105. values: [1, 2, 3, 4],
  106. } as const)
  107. ).toEqualTypeOf<{
  108. readonly type: PropType<1 | 2 | 3 | 4>
  109. readonly required: false
  110. readonly validator: ((val: unknown) => boolean) | undefined
  111. [epPropKey]: true
  112. }>()
  113. })
  114. it('Type and values', () => {
  115. expectTypeOf(
  116. buildProp({
  117. type: Number,
  118. values: [1, 2, 3, 4],
  119. } as const)
  120. ).toEqualTypeOf<{
  121. readonly type: PropType<1 | 2 | 3 | 4>
  122. readonly required: false
  123. readonly validator: ((val: unknown) => boolean) | undefined
  124. [epPropKey]: true
  125. }>()
  126. })
  127. it('Values and validator', () => {
  128. expectTypeOf(
  129. buildProp({
  130. values: ['a', 'b', 'c'],
  131. validator: (val: unknown): val is number => typeof val === 'number',
  132. } as const)
  133. ).toEqualTypeOf<{
  134. readonly type: PropType<number | 'a' | 'b' | 'c'>
  135. readonly required: false
  136. readonly validator: ((val: unknown) => boolean) | undefined
  137. [epPropKey]: true
  138. }>()
  139. })
  140. it('Values and required', () => {
  141. expectTypeOf(
  142. buildProp({
  143. values: ['a', 'b', 'c'],
  144. required: true,
  145. } as const)
  146. ).toEqualTypeOf<{
  147. readonly type: PropType<'a' | 'b' | 'c'>
  148. readonly required: true
  149. readonly validator: ((val: unknown) => boolean) | undefined
  150. [epPropKey]: true
  151. }>()
  152. })
  153. it('Value and default', () => {
  154. expectTypeOf(
  155. buildProp({
  156. values: ['a', 'b', 'c'],
  157. required: false,
  158. default: 'b',
  159. } as const)
  160. ).toEqualTypeOf<{
  161. readonly type: PropType<'a' | 'b' | 'c'>
  162. readonly required: false
  163. readonly default: 'b'
  164. readonly validator: ((val: unknown) => boolean) | undefined
  165. [epPropKey]: true
  166. }>()
  167. })
  168. it('Type and Array default value', () => {
  169. expectTypeOf(
  170. buildProp({
  171. type: definePropType<string[]>(Array),
  172. default: () => mutable(['a', 'b'] as const),
  173. } as const)
  174. ).toEqualTypeOf<{
  175. readonly type: PropType<string[]>
  176. readonly required: false
  177. readonly default: ['a', 'b']
  178. readonly validator: ((val: unknown) => boolean) | undefined
  179. [epPropKey]: true
  180. }>()
  181. })
  182. it('Type and Object default value', () => {
  183. interface Options {
  184. key: string
  185. }
  186. expectTypeOf(
  187. buildProp({
  188. type: definePropType<Options>(Object),
  189. default: () => mutable({ key: 'value' } as const),
  190. } as const)
  191. ).toEqualTypeOf<{
  192. readonly type: PropType<Options>
  193. readonly required: false
  194. readonly default: { key: 'value' }
  195. readonly validator: ((val: unknown) => boolean) | undefined
  196. [epPropKey]: true
  197. }>()
  198. })
  199. it('Type, validator and Object default value', () => {
  200. interface Options {
  201. key: string
  202. }
  203. expectTypeOf(
  204. buildProp({
  205. type: definePropType<Options>(Object),
  206. default: () => ({ key: 'value' }),
  207. validator: (val: unknown): val is string => true,
  208. } as const)
  209. ).toEqualTypeOf<{
  210. readonly type: PropType<string | Options>
  211. readonly required: false
  212. readonly default: { key: string }
  213. readonly validator: ((val: unknown) => boolean) | undefined
  214. [epPropKey]: true
  215. }>()
  216. })
  217. it('Type, validator, required', () => {
  218. expectTypeOf(
  219. buildProp({
  220. type: definePropType<'a' | 'b' | 'c'>(String),
  221. required: true,
  222. validator: (val: unknown): val is number => true,
  223. } as const)
  224. ).toEqualTypeOf<{
  225. readonly type: PropType<number | 'a' | 'b' | 'c'>
  226. readonly required: true
  227. readonly validator: ((val: unknown) => boolean) | undefined
  228. [epPropKey]: true
  229. }>()
  230. })
  231. it('Normal type', () => {
  232. expectTypeOf(
  233. buildProp({
  234. type: String,
  235. })
  236. ).toEqualTypeOf<{
  237. readonly type: PropType<string>
  238. readonly required: false
  239. readonly validator: ((val: unknown) => boolean) | undefined
  240. [epPropKey]: true
  241. }>()
  242. })
  243. it('Normal types', () => {
  244. expectTypeOf(buildProp({ type: [String, Number, Boolean] })).toEqualTypeOf<{
  245. readonly type: PropType<string | number | boolean>
  246. readonly required: false
  247. readonly validator: ((val: unknown) => boolean) | undefined
  248. [epPropKey]: true
  249. }>()
  250. })
  251. it('Normal type and values', () => {
  252. expectTypeOf(
  253. buildProp({
  254. type: String,
  255. values: ['1', '2', '3'],
  256. } as const)
  257. ).toEqualTypeOf<{
  258. readonly type: PropType<'1' | '2' | '3'>
  259. readonly required: false
  260. readonly validator: ((val: unknown) => boolean) | undefined
  261. [epPropKey]: true
  262. }>()
  263. })
  264. it('Required and validator', () => {
  265. expectTypeOf(
  266. buildProp({
  267. required: true,
  268. validator: (val: unknown): val is string => true,
  269. } as const)
  270. ).toEqualTypeOf<{
  271. readonly type: PropType<string>
  272. readonly required: true
  273. readonly validator: ((val: unknown) => boolean) | undefined
  274. [epPropKey]: true
  275. }>()
  276. })
  277. it('Required and validator', () => {
  278. expectTypeOf(
  279. buildProp({
  280. values: keysOf({ a: 'a', b: 'b' }),
  281. default: 'a',
  282. } as const)
  283. ).toEqualTypeOf<{
  284. readonly type: PropType<'a' | 'b'>
  285. readonly required: false
  286. readonly default: 'a'
  287. readonly validator: ((val: unknown) => boolean) | undefined
  288. [epPropKey]: true
  289. }>()
  290. })
  291. it('Type and default value', () => {
  292. expectTypeOf(
  293. buildProp({
  294. type: definePropType<{ key: 'a' | 'b' | 'c' } | undefined>(Object),
  295. default: () => mutable({ key: 'a' } as const),
  296. } as const)
  297. ).toEqualTypeOf<{
  298. readonly type: PropType<{ key: 'a' | 'b' | 'c' } | undefined>
  299. readonly required: false
  300. readonly default: { key: 'a' }
  301. readonly validator: ((val: unknown) => boolean) | undefined
  302. [epPropKey]: true
  303. }>()
  304. })
  305. it('Type and default value', () => {
  306. expectTypeOf(
  307. buildProp({
  308. type: [String, Number],
  309. default: '',
  310. } as const)
  311. ).toEqualTypeOf<{
  312. readonly type: PropType<string | number>
  313. readonly required: false
  314. readonly default: ''
  315. readonly validator: ((val: unknown) => boolean) | undefined
  316. [epPropKey]: true
  317. }>()
  318. })
  319. it('default value is empty object', () => {
  320. expectTypeOf(
  321. buildProp({
  322. type: Object,
  323. default: () => mutable({} as const),
  324. } as const)
  325. ).toEqualTypeOf<{
  326. readonly type: PropType<Record<string, any>>
  327. readonly required: false
  328. readonly default: {}
  329. readonly validator: ((val: unknown) => boolean) | undefined
  330. [epPropKey]: true
  331. }>()
  332. })
  333. it('extract', () => {
  334. const props = {
  335. key1: buildProp({
  336. type: String,
  337. required: true,
  338. }),
  339. key2: buildProp({
  340. type: [String, Number],
  341. required: true,
  342. }),
  343. } as const
  344. type Extracted = ExtractPropTypes<typeof props>
  345. expectTypeOf<Extracted>().toEqualTypeOf<{
  346. readonly key1: string
  347. readonly key2: string | number
  348. }>()
  349. })
  350. })
  351. describe('buildProps', () => {
  352. it('test buildProps', () => {
  353. const propsCommon = buildProps({
  354. type: {
  355. type: String,
  356. default: 'hello',
  357. },
  358. } as const)
  359. const props = buildProps({
  360. ...propsCommon,
  361. key1: {
  362. type: definePropType<'a' | 'b'>(String),
  363. },
  364. key2: {
  365. values: [1, 2, 3, 4],
  366. },
  367. key3: {
  368. values: [1, 2, 3, 4],
  369. default: 2,
  370. },
  371. key4: {
  372. values: keysOf({ a: 'a', b: 'b' }),
  373. default: 'a',
  374. },
  375. key5: Boolean,
  376. key6: String,
  377. key7: null,
  378. key8: Object,
  379. key9: Date,
  380. key10: Set,
  381. key11: undefined,
  382. // nested
  383. key12: buildProp({
  384. type: String,
  385. } as const),
  386. // default generator
  387. key13: {
  388. type: [String, Number, Function],
  389. default: () => '123' as const,
  390. } as const,
  391. key14: {
  392. type: Function,
  393. default: () => '123' as const,
  394. } as const,
  395. key15: {
  396. type: Function,
  397. default: () => () => '123' as const,
  398. } as const,
  399. key16: {
  400. type: String,
  401. default: () => '123' as const,
  402. } as const,
  403. } as const)
  404. expectTypeOf(props.type).toEqualTypeOf<{
  405. readonly type: PropType<string>
  406. readonly required: false
  407. readonly default: 'hello'
  408. readonly validator: ((val: unknown) => boolean) | undefined
  409. [epPropKey]: true
  410. }>()
  411. expectTypeOf(props.key1).toEqualTypeOf<{
  412. readonly type: PropType<'a' | 'b'>
  413. readonly required: false
  414. readonly validator: ((val: unknown) => boolean) | undefined
  415. [epPropKey]: true
  416. }>()
  417. expectTypeOf(props.key2).toEqualTypeOf<{
  418. readonly type: PropType<1 | 2 | 3 | 4>
  419. readonly required: false
  420. readonly validator: ((val: unknown) => boolean) | undefined
  421. [epPropKey]: true
  422. }>()
  423. expectTypeOf(props.key3).toEqualTypeOf<{
  424. readonly type: PropType<1 | 2 | 3 | 4>
  425. readonly required: false
  426. readonly default: 2
  427. readonly validator: ((val: unknown) => boolean) | undefined
  428. [epPropKey]: true
  429. }>()
  430. expectTypeOf(props.key4).toEqualTypeOf<{
  431. readonly type: PropType<'a' | 'b'>
  432. readonly required: false
  433. readonly default: 'a'
  434. readonly validator: ((val: unknown) => boolean) | undefined
  435. [epPropKey]: true
  436. }>()
  437. expectTypeOf(props.key5).toEqualTypeOf<BooleanConstructor>()
  438. expectTypeOf(props.key6).toEqualTypeOf<StringConstructor>()
  439. expectTypeOf(props.key7).toEqualTypeOf<null>()
  440. expectTypeOf(props.key8).toEqualTypeOf<ObjectConstructor>()
  441. expectTypeOf(props.key9).toEqualTypeOf<DateConstructor>()
  442. expectTypeOf(props.key10).toEqualTypeOf<SetConstructor>()
  443. expectTypeOf(props.key11).toEqualTypeOf<undefined>()
  444. expectTypeOf(props.key12).toEqualTypeOf<{
  445. readonly type: PropType<string>
  446. readonly required: false
  447. readonly validator: ((val: unknown) => boolean) | undefined
  448. [epPropKey]: true
  449. }>()
  450. expectTypeOf(props.key13).toEqualTypeOf<{
  451. readonly type: PropType<string | number | Function>
  452. readonly required: false
  453. // TODO
  454. readonly default: () => '123'
  455. readonly validator: ((val: unknown) => boolean) | undefined
  456. [epPropKey]: true
  457. }>()
  458. expectTypeOf(props.key14).toEqualTypeOf<{
  459. readonly type: PropType<Function>
  460. readonly required: false
  461. readonly default: () => '123'
  462. readonly validator: ((val: unknown) => boolean) | undefined
  463. [epPropKey]: true
  464. }>()
  465. expectTypeOf(props.key15).toEqualTypeOf<{
  466. readonly type: PropType<Function>
  467. readonly required: false
  468. readonly default: () => () => '123'
  469. readonly validator: ((val: unknown) => boolean) | undefined
  470. [epPropKey]: true
  471. }>()
  472. expectTypeOf(props.key16).toEqualTypeOf<{
  473. readonly type: PropType<string>
  474. readonly required: false
  475. // TODO
  476. readonly default: () => '123'
  477. readonly validator: ((val: unknown) => boolean) | undefined
  478. [epPropKey]: true
  479. }>()
  480. })
  481. })
  482. describe('runtime', () => {
  483. it('default value', () => {
  484. const warnHandler = vi.fn()
  485. const Foo = defineComponent({
  486. props: buildProps({
  487. bar: { type: Boolean },
  488. baz: { values: ['a', 'b', 'c'] },
  489. qux: { values: ['a', 'b', 'c'], required: true },
  490. qux2: { values: ['a', 'b', 'c'], required: true },
  491. } as const),
  492. template: `{{ $props }}`,
  493. })
  494. const props = mount(Foo as any, {
  495. props: {
  496. baz: undefined,
  497. qux2: undefined,
  498. },
  499. global: {
  500. config: {
  501. warnHandler,
  502. },
  503. },
  504. }).props()
  505. expect(props.bar).toBe(false)
  506. expect(props.baz).toBe(undefined)
  507. expect(warnHandler.mock.calls[0][0]).toBe('Missing required prop: "qux"')
  508. expect(warnHandler.mock.calls[1][0]).toBe(
  509. 'Invalid prop: validation failed for prop "qux2". Expected one of ["a", "b", "c"], got value undefined.'
  510. )
  511. })
  512. })