componentPublicInstance.spec.ts 12 KB

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