componentPublicInstance.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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. const v3 = instanceProxy.toggle()
  305. expect(v3).toEqual('b')
  306. expect(spy).toHaveBeenCalled()
  307. expect(getCalledTimes).toEqual(3)
  308. })
  309. test('defineProperty on proxy property with value descriptor', () => {
  310. // #5417
  311. let instanceProxy: any
  312. const Comp = {
  313. render() {},
  314. setup() {
  315. return {
  316. toggle: 'a',
  317. }
  318. },
  319. mounted() {
  320. instanceProxy = this
  321. },
  322. }
  323. const app = createApp(Comp)
  324. app.mount(nodeOps.createElement('div'))
  325. const v1 = instanceProxy.toggle
  326. expect(v1).toEqual('a')
  327. Object.defineProperty(instanceProxy, 'toggle', {
  328. value: 'b',
  329. })
  330. const v2 = instanceProxy.toggle
  331. expect(v2).toEqual('b')
  332. // expect null to be a settable value
  333. Object.defineProperty(instanceProxy, 'toggle', {
  334. value: null,
  335. })
  336. const v3 = instanceProxy.toggle
  337. expect(v3).toBeNull()
  338. })
  339. test('defineProperty on public instance proxy should work with SETUP,DATA,CONTEXT,PROPS', () => {
  340. // #5417
  341. let instanceProxy: any
  342. const Comp = {
  343. props: ['fromProp'],
  344. data() {
  345. return { name: 'data.name' }
  346. },
  347. computed: {
  348. greet() {
  349. return 'Hi ' + (this as any).name
  350. },
  351. },
  352. render() {},
  353. setup() {
  354. return {
  355. fromSetup: true,
  356. }
  357. },
  358. mounted() {
  359. instanceProxy = this
  360. },
  361. }
  362. const app = createApp(Comp, {
  363. fromProp: true,
  364. })
  365. app.mount(nodeOps.createElement('div'))
  366. expect(instanceProxy.greet).toEqual('Hi data.name')
  367. // define property on data
  368. Object.defineProperty(instanceProxy, 'name', {
  369. get() {
  370. return 'getter.name'
  371. },
  372. })
  373. // computed is same still cached
  374. expect(instanceProxy.greet).toEqual('Hi data.name')
  375. // trigger computed
  376. instanceProxy.name = ''
  377. // expect "greet" to evaluated and use name from context getter
  378. expect(instanceProxy.greet).toEqual('Hi getter.name')
  379. // defineProperty on computed ( context )
  380. Object.defineProperty(instanceProxy, 'greet', {
  381. get() {
  382. return 'Hi greet.getter.computed'
  383. },
  384. })
  385. expect(instanceProxy.greet).toEqual('Hi greet.getter.computed')
  386. // defineProperty on setupState
  387. expect(instanceProxy.fromSetup).toBe(true)
  388. Object.defineProperty(instanceProxy, 'fromSetup', {
  389. get() {
  390. return false
  391. },
  392. })
  393. expect(instanceProxy.fromSetup).toBe(false)
  394. // defineProperty on Props
  395. expect(instanceProxy.fromProp).toBe(true)
  396. Object.defineProperty(instanceProxy, 'fromProp', {
  397. get() {
  398. return false
  399. },
  400. })
  401. expect(instanceProxy.fromProp).toBe(false)
  402. })
  403. // #864
  404. test('should not warn declared but absent props', () => {
  405. const Comp = {
  406. props: ['test'],
  407. render(this: any) {
  408. return this.test
  409. },
  410. }
  411. render(h(Comp), nodeOps.createElement('div'))
  412. expect(
  413. `was accessed during render but is not defined`,
  414. ).not.toHaveBeenWarned()
  415. })
  416. test('should allow symbol to access on render', () => {
  417. const Comp = {
  418. render() {
  419. if ((this as any)[Symbol.unscopables]) {
  420. return '1'
  421. }
  422. return '2'
  423. },
  424. }
  425. const app = createApp(Comp)
  426. app.mount(nodeOps.createElement('div'))
  427. expect(
  428. `Property ${JSON.stringify(
  429. Symbol.unscopables,
  430. )} was accessed during render ` + `but is not defined on instance.`,
  431. ).toHaveBeenWarned()
  432. })
  433. test('should prevent mutating script setup bindings', () => {
  434. const Comp = defineComponent({
  435. render() {},
  436. setup() {
  437. return {
  438. __isScriptSetup: true,
  439. foo: 1,
  440. }
  441. },
  442. mounted() {
  443. expect('foo' in this).toBe(false)
  444. try {
  445. this.foo = 123
  446. } catch (e) {}
  447. },
  448. })
  449. render(h(Comp), nodeOps.createElement('div'))
  450. expect(`Cannot mutate <script setup> binding "foo"`).toHaveBeenWarned()
  451. })
  452. })