Skip to content

Commit db9fb9a

Browse files
authored
Merge pull request #212 from vuejs/issue-210-is-visible
Add isVisible and stub transition/transition-group by default
2 parents 3110633 + eeb74c3 commit db9fb9a

File tree

8 files changed

+262
-10
lines changed

8 files changed

+262
-10
lines changed

src/config.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { ComponentPublicInstance } from 'vue'
12
import { GlobalMountOptions } from './types'
23
import { VueWrapper } from './vueWrapper'
3-
import { ComponentPublicInstance } from 'vue'
44

55
interface GlobalConfigOptions {
66
global: GlobalMountOptions
@@ -19,7 +19,7 @@ interface Plugin {
1919
}
2020

2121
class Pluggable {
22-
installedPlugins = [] as Array<Plugin>
22+
installedPlugins: Plugin[] = []
2323

2424
install(
2525
handler: (
@@ -55,7 +55,12 @@ class Pluggable {
5555
}
5656

5757
export const config: GlobalConfigOptions = {
58-
global: {},
58+
global: {
59+
stubs: {
60+
transition: true,
61+
'transition-group': true
62+
}
63+
},
5964
plugins: {
6065
VueWrapper: new Pluggable(),
6166
DOMWrapper: new Pluggable()

src/domWrapper.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { nextTick } from 'vue'
22

33
import { createWrapperError } from './errorWrapper'
44
import { TriggerOptions, createDOMEvent } from './createDomEvent'
5+
import { isElementVisible } from './utils/isElementVisible'
56

67
export class DOMWrapper<ElementType extends Element> {
78
element: ElementType
@@ -36,6 +37,10 @@ export class DOMWrapper<ElementType extends Element> {
3637
return true
3738
}
3839

40+
isVisible() {
41+
return isElementVisible(this.element)
42+
}
43+
3944
text() {
4045
return this.element.textContent?.trim()
4146
}

src/mount.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,11 +350,9 @@ export function mount(
350350
app.mixin(attachEmitListener())
351351

352352
// stubs
353-
if (global.stubs || options?.shallow) {
354-
stubComponents(global.stubs, options?.shallow)
355-
} else {
356-
transformVNodeArgs()
357-
}
353+
// even if we are using `mount`, we will still
354+
// stub out Transition and Transition Group by default.
355+
stubComponents(global.stubs, options?.shallow)
358356

359357
// mount the app!
360358
const vm = app.mount(el)

src/stubs.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
transformVNodeArgs,
3+
Transition,
4+
TransitionGroup,
35
h,
46
ComponentPublicInstance,
57
Slots,
@@ -34,6 +36,17 @@ const createStub = ({ name, props }: StubOptions): ComponentOptions => {
3436
return defineComponent({ name: name || anonName, render, props })
3537
}
3638

39+
const createTransitionStub = ({
40+
name,
41+
props
42+
}: StubOptions): ComponentOptions => {
43+
const render = (ctx: ComponentPublicInstance) => {
44+
return h(name, {}, ctx.$slots)
45+
}
46+
47+
return defineComponent({ name, render, props })
48+
}
49+
3750
const resolveComponentStubByName = (
3851
componentName: string,
3952
stubs: Record<any, any>
@@ -78,6 +91,28 @@ export function stubComponents(
7891
transformVNodeArgs((args, instance: ComponentInternalInstance | null) => {
7992
const [nodeType, props, children, patchFlag, dynamicProps] = args
8093
const type = nodeType as VNodeTypes
94+
95+
// stub transition by default via config.global.stubs
96+
if (type === Transition && stubs['transition']) {
97+
return [
98+
createTransitionStub({ name: 'transition-stub', props: undefined }),
99+
undefined,
100+
children
101+
]
102+
}
103+
104+
// stub transition-group by default via config.global.stubs
105+
if (type === TransitionGroup && stubs['transition-group']) {
106+
return [
107+
createTransitionStub({
108+
name: 'transition-group-stub',
109+
props: undefined
110+
}),
111+
undefined,
112+
children
113+
]
114+
}
115+
81116
// args[0] can either be:
82117
// 1. a HTML tag (div, span...)
83118
// 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} }

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type GlobalMountOptions = {
2828
components?: Record<string, Component | object>
2929
directives?: Record<string, Directive>
3030
stubs?: Record<any, any>
31+
renderStubDefaultSlot?: boolean
3132
}
3233

3334
export interface VueWrapperMeta {

src/utils/isElementVisible.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*!
2+
* isElementVisible
3+
* Adapted from https://github.com/testing-library/jest-dom
4+
* Licensed under the MIT License.
5+
*/
6+
7+
function isStyleVisible<T extends Element>(element: T) {
8+
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
9+
return false
10+
}
11+
12+
const { display, visibility, opacity } = element.style
13+
14+
return (
15+
display !== 'none' &&
16+
visibility !== 'hidden' &&
17+
visibility !== 'collapse' &&
18+
opacity !== '0'
19+
)
20+
}
21+
22+
function isAttributeVisible<T extends Element>(element: T) {
23+
return (
24+
!element.hasAttribute('hidden') &&
25+
(element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true)
26+
)
27+
}
28+
29+
export function isElementVisible<T extends Element>(element: T) {
30+
return (
31+
element.nodeName !== '#comment' &&
32+
isStyleVisible(element) &&
33+
isAttributeVisible(element) &&
34+
(!element.parentElement || isElementVisible(element.parentElement))
35+
)
36+
}

tests/isVisible.spec.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { mount } from '../src'
2+
3+
describe('isVisible', () => {
4+
const Comp = {
5+
template: `<div><span v-show="show" /></div>`,
6+
props: {
7+
show: {
8+
type: Boolean
9+
}
10+
}
11+
}
12+
13+
it('returns false when element hidden via v-show', () => {
14+
const wrapper = mount(Comp, {
15+
props: {
16+
show: false
17+
}
18+
})
19+
20+
expect(wrapper.find('span').isVisible()).toBe(false)
21+
})
22+
23+
it('returns true when element is visible via v-show', () => {
24+
const wrapper = mount(Comp, {
25+
props: {
26+
show: true
27+
}
28+
})
29+
30+
expect(wrapper.find('span').isVisible()).toBe(true)
31+
})
32+
33+
it('returns false when element parent is invisible via v-show', () => {
34+
const Comp = {
35+
template: `<div v-show="false"><span /></div>`
36+
}
37+
const wrapper = mount(Comp)
38+
39+
expect(wrapper.find('span').isVisible()).toBe(false)
40+
})
41+
42+
it('element becomes hidden reactively', async () => {
43+
const Comp = {
44+
template: `<button @click="show = false" /><span v-show="show" />`,
45+
data() {
46+
return {
47+
show: true
48+
}
49+
}
50+
}
51+
const wrapper = mount(Comp)
52+
53+
expect(wrapper.find('span').isVisible()).toBe(true)
54+
await wrapper.find('button').trigger('click')
55+
expect(wrapper.find('span').isVisible()).toBe(false)
56+
})
57+
58+
it('handles transitions', async () => {
59+
const Comp = {
60+
template: `
61+
<button @click="show = false" />
62+
<transition name="fade">
63+
<span class="item" v-show="show">
64+
Content
65+
</span>
66+
</transition>
67+
`,
68+
data() {
69+
return {
70+
show: true
71+
}
72+
}
73+
}
74+
const wrapper = mount(Comp, {})
75+
76+
expect(wrapper.find('span').isVisible()).toBe(true)
77+
await wrapper.find('button').trigger('click')
78+
expect(wrapper.find('span').isVisible()).toBe(false)
79+
})
80+
81+
it('handles transition-group', async () => {
82+
const Comp = {
83+
template: `
84+
<div id="list-demo">
85+
<button @click="add" id="add">Add</button>
86+
<button @click="remove" id="remove">Remove</button>
87+
<transition-group name="list" tag="p">
88+
<span v-for="item in items" :key="item" class="list-item">
89+
Item: {{ item }}
90+
</span>
91+
</transition-group>
92+
</div>
93+
`,
94+
methods: {
95+
add() {
96+
this.items.push(2)
97+
},
98+
remove() {
99+
this.items.splice(1) // back to [1]
100+
}
101+
},
102+
data() {
103+
return {
104+
items: [1]
105+
}
106+
}
107+
}
108+
const wrapper = mount(Comp)
109+
110+
expect(wrapper.html()).toContain('Item: 1')
111+
await wrapper.find('#add').trigger('click')
112+
expect(wrapper.html()).toContain('Item: 1')
113+
expect(wrapper.html()).toContain('Item: 2')
114+
await wrapper.find('#remove').trigger('click')
115+
expect(wrapper.html()).toContain('Item: 1')
116+
expect(wrapper.html()).not.toContain('Item: 2')
117+
})
118+
})

tests/mountingOptions/stubs.global.spec.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import ComponentWithoutName from '../components/ComponentWithoutName.vue'
66
import ComponentWithSlots from '../components/ComponentWithSlots.vue'
77

88
describe('mounting options: stubs', () => {
9+
let configStubsSave = config.global.stubs
910
beforeEach(() => {
10-
config.global.stubs = {}
11+
config.global.stubs = configStubsSave
1112
})
1213

1314
afterEach(() => {
14-
config.global.stubs = {}
15+
config.global.stubs = configStubsSave
1516
})
1617

1718
it('handles Array syntax', () => {
@@ -308,6 +309,59 @@ describe('mounting options: stubs', () => {
308309
expect(wrapper.html()).toBe('<foo-bar-stub></foo-bar-stub>')
309310
})
310311

312+
it('stubs transition by default', () => {
313+
const Comp = {
314+
template: `<transition><div id="content" /></transition>`
315+
}
316+
const wrapper = mount(Comp)
317+
318+
expect(wrapper.html()).toBe(
319+
'<transition-stub><div id="content"></div></transition-stub>'
320+
)
321+
})
322+
323+
it('opts out of stubbing transition by default', () => {
324+
const Comp = {
325+
template: `<transition><div id="content" /></transition>`
326+
}
327+
const wrapper = mount(Comp, {
328+
global: {
329+
stubs: {
330+
transition: false
331+
}
332+
}
333+
})
334+
335+
// Vue removes <transition> at run-time and does it's magic, so <transition> should not
336+
// appear in the html when it isn't stubbed.
337+
expect(wrapper.html()).toBe('<div id="content"></div>')
338+
})
339+
340+
it('opts out of stubbing transition-group by default', () => {
341+
const Comp = {
342+
template: `<transition-group><div key="content" id="content" /></transition-group>`
343+
}
344+
const wrapper = mount(Comp, {
345+
global: {
346+
stubs: {
347+
'transition-group': false
348+
}
349+
}
350+
})
351+
352+
// Vue removes <transition-group> at run-time and does it's magic, so <transition-group> should not
353+
// appear in the html when it isn't stubbed.
354+
expect(wrapper.html()).toBe('<div id="content"></div>')
355+
})
356+
357+
it('stubs transition-group by default', () => {
358+
const Comp = {
359+
template: `<transition-group><div key="a" id="content" /></transition-group>`
360+
}
361+
const wrapper = mount(Comp)
362+
expect(wrapper.find('#content').exists()).toBe(true)
363+
})
364+
311365
describe('stub slots', () => {
312366
const Component = {
313367
name: 'Parent',

0 commit comments

Comments
 (0)