@dimailn/vuetify 2.7.2-alpha51 → 2.7.2-alpha53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,16 @@
1
1
  // Components
2
2
  import VListItem from '../VListItem'
3
3
 
4
+ // Libraries
5
+ import { defineComponent, h, nextTick } from 'vue'
6
+
4
7
  // Utilities
5
8
  import {
6
9
  mount,
7
10
  VueWrapper,
8
11
  enableAutoUnmount
9
12
  } from '@vue/test-utils'
13
+ import { createRouter, createWebHistory } from 'vue-router'
10
14
  import { Vue3RouterLinkStub } from '../../../../test/util/stubs'
11
15
 
12
16
  describe('VListItem.ts', () => {
@@ -325,4 +329,119 @@ describe('VListItem.ts', () => {
325
329
 
326
330
  expect(wrapper.vm.isClickable).toBe(true)
327
331
  })
332
+
333
+ it('passes active and toggle in default slot scope', async () => {
334
+ const wrapper = mountFunction({
335
+ props: { modelValue: true },
336
+ slots: {
337
+ default: ({ active, toggle }: { active: boolean, toggle: Function }) => h('div', [
338
+ h('span', { class: { 'link--text': active } }, String(active)),
339
+ h('button', { onClick: toggle }, 'toggle')
340
+ ])
341
+ }
342
+ })
343
+
344
+ expect(wrapper.find('.link--text').exists()).toBe(true)
345
+ expect(wrapper.find('span').text()).toBe('true')
346
+ })
347
+
348
+ it('does not pass undefined to activeClass when to is used without activeClass', () => {
349
+ const RouterLinkCapture = defineComponent({
350
+ name: 'RouterLinkCapture',
351
+ props: {
352
+ to: { type: [String, Object], required: true },
353
+ activeClass: String,
354
+ exactActiveClass: String
355
+ },
356
+ setup (props, { slots }) {
357
+ return () => h('a', { class: props.activeClass }, slots.default?.())
358
+ }
359
+ })
360
+
361
+ const wrapper = mountFunction({
362
+ props: { to: '/foo' },
363
+ global: {
364
+ stubs: {
365
+ 'router-link': RouterLinkCapture
366
+ }
367
+ }
368
+ })
369
+
370
+ const routerLink = wrapper.findComponent({ name: 'RouterLinkCapture' })
371
+
372
+ expect(routerLink.props('activeClass')).toBe('v-list-item--active')
373
+ expect(routerLink.props('activeClass')).not.toContain('undefined')
374
+ })
375
+
376
+ it('syncs slot active with router-link when route matches', async () => {
377
+ const router = createRouter({
378
+ history: createWebHistory(),
379
+ routes: [
380
+ { path: '/products/:id', component: { template: '<div />' } }
381
+ ]
382
+ })
383
+
384
+ await router.push('/products/131830946')
385
+ await router.isReady()
386
+
387
+ const Host = defineComponent({
388
+ components: { VListItem },
389
+ props: { to: String },
390
+ template: `
391
+ <v-list-item :to="to">
392
+ <template #default="{ active }">
393
+ <span :class="{ 'link--text': active }">{{ active }}</span>
394
+ </template>
395
+ </v-list-item>
396
+ `
397
+ })
398
+
399
+ const wrapper = mount(Host, {
400
+ props: { to: '/products/131830946' },
401
+ global: {
402
+ plugins: [router],
403
+ provide: { isInGroup: true },
404
+ stubs: {
405
+ 'router-link': Vue3RouterLinkStub
406
+ }
407
+ }
408
+ })
409
+
410
+ const listItem = wrapper.findComponent(VListItem)
411
+
412
+ ;(listItem.vm.$refs.link as any)._vnode = {
413
+ data: {
414
+ class: { 'v-list-item--active': true }
415
+ }
416
+ }
417
+
418
+ listItem.vm.onRouteChange()
419
+ await nextTick()
420
+ await nextTick()
421
+
422
+ expect(listItem.vm.isActive).toBe(true)
423
+ expect(wrapper.find('.link--text').exists()).toBe(true)
424
+ expect(listItem.element.getAttribute('aria-selected')).toBe('true')
425
+ })
426
+
427
+ it('syncs aria-selected with isActive in list item group', async () => {
428
+ const wrapper = mountFunction({
429
+ props: { modelValue: 'item-1' },
430
+ global: {
431
+ provide: {
432
+ isInGroup: true,
433
+ listItemGroup: {
434
+ activeClass: 'v-item--active',
435
+ register: () => {},
436
+ unregister: () => {}
437
+ }
438
+ }
439
+ }
440
+ })
441
+
442
+ wrapper.vm.isActive = true
443
+ await wrapper.vm.$nextTick()
444
+
445
+ expect(wrapper.element.getAttribute('aria-selected')).toBe('true')
446
+ })
328
447
  })
@@ -1,7 +1,7 @@
1
1
  import Routable from '../'
2
2
  import { mount, Wrapper } from '@vue/test-utils'
3
3
  import { createRouter, createWebHistory } from 'vue-router'
4
- import { nextTick } from 'vue'
4
+ import { defineComponent, h, nextTick } from 'vue'
5
5
 
6
6
  describe('routable.ts', () => {
7
7
  let mountFunction: (options?: object) => Wrapper<any>
@@ -102,4 +102,81 @@ describe('routable.ts', () => {
102
102
  expect(wrapper.vm.isLink).toBe(true)
103
103
  expect(wrapper.vm.isClickable).toBe(true)
104
104
  })
105
+
106
+ it('should not include undefined in activeClass passed to router-link', async () => {
107
+ const RouterLinkCapture = defineComponent({
108
+ name: 'RouterLinkCapture',
109
+ props: {
110
+ to: { type: [String, Object], required: true },
111
+ activeClass: String,
112
+ exactActiveClass: String
113
+ },
114
+ template: '<a><slot /></a>'
115
+ })
116
+
117
+ const wrapper = mount({
118
+ mixins: [Routable],
119
+ data: () => ({
120
+ proxyClass: 'v-list-item--active'
121
+ }),
122
+ render () {
123
+ const { tag, data } = this.generateRouteLink()
124
+ return h(tag, data, { default: () => 'link' })
125
+ }
126
+ }, {
127
+ global: {
128
+ plugins: [router],
129
+ stubs: {
130
+ 'router-link': RouterLinkCapture
131
+ }
132
+ },
133
+ props: {
134
+ to: '/'
135
+ }
136
+ })
137
+
138
+ const routerLink = wrapper.findComponent({ name: 'RouterLinkCapture' })
139
+
140
+ expect(routerLink.props('activeClass')).toBe('v-list-item--active')
141
+ expect(routerLink.props('activeClass')).not.toContain('undefined')
142
+ expect(routerLink.props('exactActiveClass')).toBe('v-list-item--active')
143
+ })
144
+
145
+ it('should sync isActive with router-link on route change', async () => {
146
+ const toggle = jest.fn()
147
+ const wrapper = mount({
148
+ mixins: [Routable],
149
+ data: () => ({
150
+ proxyClass: 'v-tab--active'
151
+ }),
152
+ methods: {
153
+ toggle
154
+ },
155
+ template: '<div ref="link" />'
156
+ }, {
157
+ global: {
158
+ plugins: [router]
159
+ },
160
+ props: {
161
+ to: '/foo',
162
+ activeClass: 'bar'
163
+ }
164
+ })
165
+
166
+ wrapper.vm.onRouteChange()
167
+ await nextTick()
168
+
169
+ expect(toggle).not.toHaveBeenCalled()
170
+
171
+ ;(wrapper.vm.$refs.link as any)._vnode = {
172
+ data: {
173
+ class: { 'bar v-tab--active': true }
174
+ }
175
+ }
176
+
177
+ wrapper.vm.onRouteChange()
178
+ await nextTick()
179
+
180
+ expect(toggle).toHaveBeenCalledTimes(1)
181
+ })
105
182
  })
@@ -119,8 +119,8 @@ export default defineComponent({
119
119
  let exactActiveClass = this.exactActiveClass || activeClass
120
120
 
121
121
  if (this.proxyClass) {
122
- activeClass = `${activeClass} ${this.proxyClass}`.trim()
123
- exactActiveClass = `${exactActiveClass} ${this.proxyClass}`.trim()
122
+ activeClass = [activeClass, this.proxyClass].filter(Boolean).join(' ')
123
+ exactActiveClass = [exactActiveClass, this.proxyClass].filter(Boolean).join(' ')
124
124
  }
125
125
 
126
126
  tag = resolveComponent(this.nuxt ? 'nuxt-link' : 'router-link')
@@ -152,7 +152,8 @@ export default defineComponent({
152
152
 
153
153
  this.$nextTick(() => {
154
154
  /* istanbul ignore else */
155
- if (!getObjectValueByPath(this.$refs.link, path) === this.isActive) {
155
+ const isLinkActive = Boolean(getObjectValueByPath(this.$refs.link, path))
156
+ if (isLinkActive !== this.isActive) {
156
157
  this.toggle()
157
158
  }
158
159
  })
@@ -48,4 +48,17 @@ describe('$vuetify.presets', () => {
48
48
  expect(JSON.stringify(itheme.themes)).toMatchSnapshot()
49
49
  expect(JSON.stringify(breakpoints.thresholds)).toMatchSnapshot()
50
50
  })
51
+
52
+ it('should prevent prototype pollution from malicious presets', () => {
53
+ const polluted = {}
54
+
55
+ expect((polluted as any).isPolluted).toBeUndefined()
56
+
57
+ const vuetify = new Framework({
58
+ preset: JSON.parse('{"__proto__":{"isPolluted":"yes"}}')
59
+ })
60
+
61
+ expect(({} as any).isPolluted).toBeUndefined()
62
+ expect((vuetify.preset as any).isPolluted).toBeUndefined()
63
+ })
51
64
  })
@@ -503,7 +503,11 @@ export function mergeDeep (
503
503
  source: Dictionary<any> = {},
504
504
  target: Dictionary<any> = {}
505
505
  ) {
506
+ const blockedKeys = ['__proto__', 'constructor', 'prototype']
507
+
506
508
  for (const key in target) {
509
+ if (blockedKeys.includes(key)) continue
510
+
507
511
  const sourceProperty = source[key]
508
512
  const targetProperty = target[key]
509
513