componentPublicInstance.spec.ts 12 KB

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