componentPublicInstance.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. import {
  2. h,
  3. render,
  4. getCurrentInstance,
  5. nodeOps,
  6. createApp,
  7. shallowReadonly
  8. } from '@vue/runtime-test'
  9. import { ComponentInternalInstance, ComponentOptions } from '../src/component'
  10. describe('component: proxy', () => {
  11. test('data', () => {
  12. let instance: ComponentInternalInstance
  13. let instanceProxy: any
  14. const Comp = {
  15. data() {
  16. return {
  17. foo: 1
  18. }
  19. },
  20. mounted() {
  21. instance = getCurrentInstance()!
  22. instanceProxy = this
  23. },
  24. render() {
  25. return null
  26. }
  27. }
  28. render(h(Comp), nodeOps.createElement('div'))
  29. expect(instanceProxy.foo).toBe(1)
  30. instanceProxy.foo = 2
  31. expect(instance!.data.foo).toBe(2)
  32. })
  33. test('setupState', () => {
  34. let instance: ComponentInternalInstance
  35. let instanceProxy: any
  36. const Comp = {
  37. setup() {
  38. return {
  39. foo: 1
  40. }
  41. },
  42. mounted() {
  43. instance = getCurrentInstance()!
  44. instanceProxy = this
  45. },
  46. render() {
  47. return null
  48. }
  49. }
  50. render(h(Comp), nodeOps.createElement('div'))
  51. expect(instanceProxy.foo).toBe(1)
  52. instanceProxy.foo = 2
  53. expect(instance!.setupState.foo).toBe(2)
  54. })
  55. test('should not expose non-declared props', () => {
  56. let instanceProxy: any
  57. const Comp = {
  58. setup() {
  59. return () => null
  60. },
  61. mounted() {
  62. instanceProxy = this
  63. }
  64. }
  65. render(h(Comp, { count: 1 }), nodeOps.createElement('div'))
  66. expect('count' in instanceProxy).toBe(false)
  67. })
  68. test('public properties', async () => {
  69. let instance: ComponentInternalInstance
  70. let instanceProxy: any
  71. const Comp = {
  72. setup() {
  73. return () => null
  74. },
  75. mounted() {
  76. instance = getCurrentInstance()!
  77. instanceProxy = this
  78. }
  79. }
  80. render(h(Comp), nodeOps.createElement('div'))
  81. expect(instanceProxy.$data).toBe(instance!.data)
  82. expect(instanceProxy.$props).toBe(shallowReadonly(instance!.props))
  83. expect(instanceProxy.$attrs).toBe(shallowReadonly(instance!.attrs))
  84. expect(instanceProxy.$slots).toBe(shallowReadonly(instance!.slots))
  85. expect(instanceProxy.$refs).toBe(shallowReadonly(instance!.refs))
  86. expect(instanceProxy.$parent).toBe(
  87. instance!.parent && instance!.parent.proxy
  88. )
  89. expect(instanceProxy.$root).toBe(instance!.root.proxy)
  90. expect(instanceProxy.$emit).toBe(instance!.emit)
  91. expect(instanceProxy.$el).toBe(instance!.vnode.el)
  92. expect(instanceProxy.$options).toBe(instance!.type as ComponentOptions)
  93. expect(() => (instanceProxy.$data = {})).toThrow(TypeError)
  94. expect(`Attempting to mutate public property "$data"`).toHaveBeenWarned()
  95. const nextTickThis = await instanceProxy.$nextTick(function (this: any) {
  96. return this
  97. })
  98. expect(nextTickThis).toBe(instanceProxy)
  99. })
  100. test('user attached properties', async () => {
  101. let instance: ComponentInternalInstance
  102. let instanceProxy: any
  103. const Comp = {
  104. setup() {
  105. return () => null
  106. },
  107. mounted() {
  108. instance = getCurrentInstance()!
  109. instanceProxy = this
  110. }
  111. }
  112. render(h(Comp), nodeOps.createElement('div'))
  113. instanceProxy.foo = 1
  114. expect(instanceProxy.foo).toBe(1)
  115. expect(instance!.ctx.foo).toBe(1)
  116. // should also allow properties that start with $
  117. const obj = (instanceProxy.$store = {})
  118. expect(instanceProxy.$store).toBe(obj)
  119. expect(instance!.ctx.$store).toBe(obj)
  120. })
  121. test('globalProperties', () => {
  122. let instance: ComponentInternalInstance
  123. let instanceProxy: any
  124. const Comp = {
  125. setup() {
  126. return () => null
  127. },
  128. mounted() {
  129. instance = getCurrentInstance()!
  130. instanceProxy = this
  131. }
  132. }
  133. const app = createApp(Comp)
  134. app.config.globalProperties.foo = 1
  135. app.mount(nodeOps.createElement('div'))
  136. expect(instanceProxy.foo).toBe(1)
  137. // set should overwrite globalProperties with local
  138. instanceProxy.foo = 2
  139. // expect(instanceProxy.foo).toBe(2)
  140. expect(instance!.ctx.foo).toBe(2)
  141. // should not affect global
  142. expect(app.config.globalProperties.foo).toBe(1)
  143. })
  144. test('has check', () => {
  145. let instanceProxy: any
  146. const Comp = {
  147. render() {},
  148. props: {
  149. msg: String
  150. },
  151. data() {
  152. return {
  153. foo: 0
  154. }
  155. },
  156. setup() {
  157. return {
  158. bar: 1
  159. }
  160. },
  161. mounted() {
  162. instanceProxy = this
  163. }
  164. }
  165. const app = createApp(Comp, { msg: 'hello' })
  166. app.config.globalProperties.global = 1
  167. app.mount(nodeOps.createElement('div'))
  168. // props
  169. expect('msg' in instanceProxy).toBe(true)
  170. // data
  171. expect('foo' in instanceProxy).toBe(true)
  172. // ctx
  173. expect('bar' in instanceProxy).toBe(true)
  174. // public properties
  175. expect('$el' in instanceProxy).toBe(true)
  176. // global properties
  177. expect('global' in instanceProxy).toBe(true)
  178. // non-existent
  179. expect('$foobar' in instanceProxy).toBe(false)
  180. expect('baz' in instanceProxy).toBe(false)
  181. // #4962 triggering getter should not cause non-existent property to
  182. // pass the has check
  183. instanceProxy.baz
  184. expect('baz' in instanceProxy).toBe(false)
  185. // set non-existent (goes into proxyTarget sink)
  186. instanceProxy.baz = 1
  187. expect('baz' in instanceProxy).toBe(true)
  188. // dev mode ownKeys check for console inspection
  189. // should only expose own keys
  190. expect(Object.keys(instanceProxy)).toMatchObject([
  191. 'msg',
  192. 'bar',
  193. 'foo',
  194. 'baz'
  195. ])
  196. })
  197. test('allow updating proxy with Object.defineProperty', () => {
  198. let instanceProxy: any
  199. const Comp = {
  200. render() {},
  201. setup() {
  202. return {
  203. isDisplayed: true
  204. }
  205. },
  206. mounted() {
  207. instanceProxy = this
  208. }
  209. }
  210. const app = createApp(Comp)
  211. app.mount(nodeOps.createElement('div'))
  212. Object.defineProperty(instanceProxy, 'isDisplayed', { value: false })
  213. expect(instanceProxy.isDisplayed).toBe(false)
  214. Object.defineProperty(instanceProxy, 'isDisplayed', { value: true })
  215. expect(instanceProxy.isDisplayed).toBe(true)
  216. Object.defineProperty(instanceProxy, 'isDisplayed', {
  217. get() {
  218. return false
  219. }
  220. })
  221. expect(instanceProxy.isDisplayed).toBe(false)
  222. Object.defineProperty(instanceProxy, 'isDisplayed', {
  223. get() {
  224. return true
  225. }
  226. })
  227. expect(instanceProxy.isDisplayed).toBe(true)
  228. })
  229. test('allow jest spying on proxy methods with Object.defineProperty', () => {
  230. // #5417
  231. let instanceProxy: any
  232. const Comp = {
  233. render() {},
  234. setup() {
  235. return {
  236. toggle() {
  237. return 'a'
  238. }
  239. }
  240. },
  241. mounted() {
  242. instanceProxy = this
  243. }
  244. }
  245. const app = createApp(Comp)
  246. app.mount(nodeOps.createElement('div'))
  247. // access 'toggle' to ensure key is cached
  248. const v1 = instanceProxy.toggle()
  249. expect(v1).toEqual('a')
  250. // reconfigure "toggle" to be getter based.
  251. let getCalledTimes = 0
  252. Object.defineProperty(instanceProxy, 'toggle', {
  253. get() {
  254. getCalledTimes++
  255. return () => 'b'
  256. }
  257. })
  258. // getter should not be evaluated on initial definition
  259. expect(getCalledTimes).toEqual(0)
  260. // invoke "toggle" after "defineProperty"
  261. const v2 = instanceProxy.toggle()
  262. expect(v2).toEqual('b')
  263. expect(getCalledTimes).toEqual(1)
  264. // expect toggle getter not to be cached. it can't be
  265. instanceProxy.toggle()
  266. expect(getCalledTimes).toEqual(2)
  267. // attaching jest spy, triggers the getter once, cache it and override the property.
  268. // also uses Object.defineProperty
  269. const spy = jest.spyOn(instanceProxy, 'toggle')
  270. expect(getCalledTimes).toEqual(3)
  271. // expect getter to not evaluate the jest spy caches its value
  272. const v3 = instanceProxy.toggle()
  273. expect(v3).toEqual('b')
  274. expect(spy).toHaveBeenCalled()
  275. expect(getCalledTimes).toEqual(3)
  276. })
  277. test('defineProperty on proxy property with value descriptor', () => {
  278. // #5417
  279. let instanceProxy: any
  280. const Comp = {
  281. render() {},
  282. setup() {
  283. return {
  284. toggle: 'a'
  285. }
  286. },
  287. mounted() {
  288. instanceProxy = this
  289. }
  290. }
  291. const app = createApp(Comp)
  292. app.mount(nodeOps.createElement('div'))
  293. const v1 = instanceProxy.toggle
  294. expect(v1).toEqual('a')
  295. Object.defineProperty(instanceProxy, 'toggle', {
  296. value: 'b'
  297. })
  298. const v2 = instanceProxy.toggle
  299. expect(v2).toEqual('b')
  300. // expect null to be a settable value
  301. Object.defineProperty(instanceProxy, 'toggle', {
  302. value: null
  303. })
  304. const v3 = instanceProxy.toggle
  305. expect(v3).toBeNull()
  306. })
  307. test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
  308. // #5417
  309. let instanceProxy: any
  310. const Comp = {
  311. props: ['fromProp'],
  312. data() {
  313. return { name: 'data.name' }
  314. },
  315. computed: {
  316. greet() {
  317. return 'Hi ' + (this as any).name
  318. }
  319. },
  320. render() {},
  321. setup() {
  322. return {
  323. fromSetup: true
  324. }
  325. },
  326. mounted() {
  327. instanceProxy = this
  328. }
  329. }
  330. const app = createApp(Comp, {
  331. fromProp: true
  332. })
  333. app.mount(nodeOps.createElement('div'))
  334. expect(instanceProxy.greet).toEqual('Hi data.name')
  335. // define property on data
  336. Object.defineProperty(instanceProxy, 'name', {
  337. get() {
  338. return 'getter.name'
  339. }
  340. })
  341. // computed is same still cached
  342. expect(instanceProxy.greet).toEqual('Hi data.name')
  343. // trigger computed
  344. instanceProxy.name = ''
  345. // expect "greet" to evaluated and use name from context getter
  346. expect(instanceProxy.greet).toEqual('Hi getter.name')
  347. // defineProperty on computed ( context )
  348. Object.defineProperty(instanceProxy, 'greet', {
  349. get() {
  350. return 'Hi greet.getter.computed'
  351. }
  352. })
  353. expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
  354. // defineProperty on setupState
  355. expect(instanceProxy.fromSetup).toBe(true)
  356. Object.defineProperty(instanceProxy, 'fromSetup', {
  357. get() {
  358. return false
  359. }
  360. })
  361. expect(instanceProxy.fromSetup).toBe(false)
  362. // defineProperty on Props
  363. expect(instanceProxy.fromProp).toBe(true)
  364. Object.defineProperty(instanceProxy, 'fromProp', {
  365. get() {
  366. return false
  367. }
  368. })
  369. expect(instanceProxy.fromProp).toBe(false)
  370. })
  371. // #864
  372. test('should not warn declared but absent props', () => {
  373. const Comp = {
  374. props: ['test'],
  375. render(this: any) {
  376. return this.test
  377. }
  378. }
  379. render(h(Comp), nodeOps.createElement('div'))
  380. expect(
  381. `was accessed during render but is not defined`
  382. ).not.toHaveBeenWarned()
  383. })
  384. test('should allow symbol to access on render', () => {
  385. const Comp = {
  386. render() {
  387. if ((this as any)[Symbol.unscopables]) {
  388. return '1'
  389. }
  390. return '2'
  391. }
  392. }
  393. const app = createApp(Comp)
  394. app.mount(nodeOps.createElement('div'))
  395. expect(
  396. `Property ${JSON.stringify(
  397. Symbol.unscopables
  398. )} was accessed during render ` + `but is not defined on instance.`
  399. ).toHaveBeenWarned()
  400. })
  401. })