componentPublicInstance.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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. $foo: 0,
  159. }
  160. },
  161. computed: {
  162. cmp: () => {
  163. throw new Error('value of cmp should not be accessed')
  164. },
  165. $cmp: () => {
  166. throw new Error('value of $cmp should not be read')
  167. },
  168. },
  169. setup() {
  170. return {
  171. bar: 1,
  172. }
  173. },
  174. __cssModules: {
  175. $style: {},
  176. cssStyles: {},
  177. },
  178. mounted() {
  179. instanceProxy = this
  180. },
  181. }
  182. const app = createApp(Comp, { msg: 'hello' })
  183. app.config.globalProperties.global = 1
  184. app.config.globalProperties.$global = 1
  185. app.mount(nodeOps.createElement('div'))
  186. // props
  187. expect('msg' in instanceProxy).toBe(true)
  188. // data
  189. expect('foo' in instanceProxy).toBe(true)
  190. expect('$foo' in instanceProxy).toBe(false)
  191. // setupState
  192. expect('bar' in instanceProxy).toBe(true)
  193. // ctx
  194. expect('cmp' in instanceProxy).toBe(true)
  195. expect('$cmp' in instanceProxy).toBe(true)
  196. // public properties
  197. expect('$el' in instanceProxy).toBe(true)
  198. // CSS modules
  199. expect('$style' in instanceProxy).toBe(true)
  200. expect('cssStyles' in instanceProxy).toBe(true)
  201. // global properties
  202. expect('global' in instanceProxy).toBe(true)
  203. expect('$global' in instanceProxy).toBe(true)
  204. // non-existent
  205. expect('$foobar' in instanceProxy).toBe(false)
  206. expect('baz' in instanceProxy).toBe(false)
  207. // #4962 triggering getter should not cause non-existent property to
  208. // pass the has check
  209. instanceProxy.baz
  210. instanceProxy.$baz
  211. expect('baz' in instanceProxy).toBe(false)
  212. expect('$baz' in instanceProxy).toBe(false)
  213. // set non-existent (goes into proxyTarget sink)
  214. instanceProxy.baz = 1
  215. expect('baz' in instanceProxy).toBe(true)
  216. instanceProxy.$baz = 1
  217. expect('$baz' in instanceProxy).toBe(true)
  218. // dev mode ownKeys check for console inspection
  219. // should only expose own keys
  220. expect(Object.keys(instanceProxy)).toMatchObject([
  221. 'msg',
  222. 'bar',
  223. 'foo',
  224. 'cmp',
  225. '$cmp',
  226. 'baz',
  227. '$baz',
  228. ])
  229. })
  230. test('allow updating proxy with Object.defineProperty', () => {
  231. let instanceProxy: any
  232. const Comp = {
  233. render() {},
  234. setup() {
  235. return {
  236. isDisplayed: true,
  237. }
  238. },
  239. mounted() {
  240. instanceProxy = this
  241. },
  242. }
  243. const app = createApp(Comp)
  244. app.mount(nodeOps.createElement('div'))
  245. Object.defineProperty(instanceProxy, 'isDisplayed', { value: false })
  246. expect(instanceProxy.isDisplayed).toBe(false)
  247. Object.defineProperty(instanceProxy, 'isDisplayed', { value: true })
  248. expect(instanceProxy.isDisplayed).toBe(true)
  249. Object.defineProperty(instanceProxy, 'isDisplayed', {
  250. get() {
  251. return false
  252. },
  253. })
  254. expect(instanceProxy.isDisplayed).toBe(false)
  255. Object.defineProperty(instanceProxy, 'isDisplayed', {
  256. get() {
  257. return true
  258. },
  259. })
  260. expect(instanceProxy.isDisplayed).toBe(true)
  261. })
  262. test('allow test runner spying on proxy methods with Object.defineProperty', () => {
  263. // #5417
  264. let instanceProxy: any
  265. const Comp = {
  266. render() {},
  267. setup() {
  268. return {
  269. toggle() {
  270. return 'a'
  271. },
  272. }
  273. },
  274. mounted() {
  275. instanceProxy = this
  276. },
  277. }
  278. const app = createApp(Comp)
  279. app.mount(nodeOps.createElement('div'))
  280. // access 'toggle' to ensure key is cached
  281. const v1 = instanceProxy.toggle()
  282. expect(v1).toEqual('a')
  283. // reconfigure "toggle" to be getter based.
  284. let getCalledTimes = 0
  285. Object.defineProperty(instanceProxy, 'toggle', {
  286. get() {
  287. getCalledTimes++
  288. return () => 'b'
  289. },
  290. })
  291. // getter should not be evaluated on initial definition
  292. expect(getCalledTimes).toEqual(0)
  293. // invoke "toggle" after "defineProperty"
  294. const v2 = instanceProxy.toggle()
  295. expect(v2).toEqual('b')
  296. expect(getCalledTimes).toEqual(1)
  297. // expect toggle getter not to be cached. it can't be
  298. instanceProxy.toggle()
  299. expect(getCalledTimes).toEqual(2)
  300. // attaching spy, triggers the getter once, and override the property.
  301. // also uses Object.defineProperty
  302. const spy = vi.spyOn(instanceProxy, 'toggle')
  303. expect(getCalledTimes).toEqual(3)
  304. // vitest does not cache the spy like jest do
  305. const v3 = instanceProxy.toggle()
  306. expect(v3).toEqual('b')
  307. expect(spy).toHaveBeenCalled()
  308. expect(getCalledTimes).toEqual(4)
  309. })
  310. test('defineProperty on proxy property with value descriptor', () => {
  311. // #5417
  312. let instanceProxy: any
  313. const Comp = {
  314. render() {},
  315. setup() {
  316. return {
  317. toggle: 'a',
  318. }
  319. },
  320. mounted() {
  321. instanceProxy = this
  322. },
  323. }
  324. const app = createApp(Comp)
  325. app.mount(nodeOps.createElement('div'))
  326. const v1 = instanceProxy.toggle
  327. expect(v1).toEqual('a')
  328. Object.defineProperty(instanceProxy, 'toggle', {
  329. value: 'b',
  330. })
  331. const v2 = instanceProxy.toggle
  332. expect(v2).toEqual('b')
  333. // expect null to be a settable value
  334. Object.defineProperty(instanceProxy, 'toggle', {
  335. value: null,
  336. })
  337. const v3 = instanceProxy.toggle
  338. expect(v3).toBeNull()
  339. })
  340. test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
  341. // #5417
  342. let instanceProxy: any
  343. const Comp = {
  344. props: ['fromProp'],
  345. data() {
  346. return { name: 'data.name' }
  347. },
  348. computed: {
  349. greet() {
  350. return 'Hi ' + (this as any).name
  351. },
  352. },
  353. render() {},
  354. setup() {
  355. return {
  356. fromSetup: true,
  357. }
  358. },
  359. mounted() {
  360. instanceProxy = this
  361. },
  362. }
  363. const app = createApp(Comp, {
  364. fromProp: true,
  365. })
  366. app.mount(nodeOps.createElement('div'))
  367. expect(instanceProxy.greet).toEqual('Hi data.name')
  368. // define property on data
  369. Object.defineProperty(instanceProxy, 'name', {
  370. get() {
  371. return 'getter.name'
  372. },
  373. })
  374. // computed is same still cached
  375. expect(instanceProxy.greet).toEqual('Hi data.name')
  376. // trigger computed
  377. instanceProxy.name = ''
  378. // expect "greet" to evaluated and use name from context getter
  379. expect(instanceProxy.greet).toEqual('Hi getter.name')
  380. // defineProperty on computed ( context )
  381. Object.defineProperty(instanceProxy, 'greet', {
  382. get() {
  383. return 'Hi greet.getter.computed'
  384. },
  385. })
  386. expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
  387. // defineProperty on setupState
  388. expect(instanceProxy.fromSetup).toBe(true)
  389. Object.defineProperty(instanceProxy, 'fromSetup', {
  390. get() {
  391. return false
  392. },
  393. })
  394. expect(instanceProxy.fromSetup).toBe(false)
  395. // defineProperty on Props
  396. expect(instanceProxy.fromProp).toBe(true)
  397. Object.defineProperty(instanceProxy, 'fromProp', {
  398. get() {
  399. return false
  400. },
  401. })
  402. expect(instanceProxy.fromProp).toBe(false)
  403. })
  404. // #864
  405. test('should not warn declared but absent props', () => {
  406. const Comp = {
  407. props: ['test'],
  408. render(this: any) {
  409. return this.test
  410. },
  411. }
  412. render(h(Comp), nodeOps.createElement('div'))
  413. expect(
  414. `was accessed during render but is not defined`,
  415. ).not.toHaveBeenWarned()
  416. })
  417. test('should allow symbol to access on render', () => {
  418. const Comp = {
  419. render() {
  420. if ((this as any)[Symbol.unscopables]) {
  421. return '1'
  422. }
  423. return '2'
  424. },
  425. }
  426. const app = createApp(Comp)
  427. app.mount(nodeOps.createElement('div'))
  428. expect(
  429. `Property ${JSON.stringify(
  430. Symbol.unscopables,
  431. )} was accessed during render ` + `but is not defined on instance.`,
  432. ).toHaveBeenWarned()
  433. })
  434. test('should prevent mutating script setup bindings', () => {
  435. const Comp = defineComponent({
  436. render() {},
  437. setup() {
  438. return {
  439. __isScriptSetup: true,
  440. foo: 1,
  441. }
  442. },
  443. mounted() {
  444. expect('foo' in this).toBe(false)
  445. try {
  446. this.foo = 123
  447. } catch (e) {}
  448. },
  449. })
  450. render(h(Comp), nodeOps.createElement('div'))
  451. expect(`Cannot mutate <script setup> binding "foo"`).toHaveBeenWarned()
  452. })
  453. })