componentPublicInstance.spec.ts 12 KB

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