Skip to content

Commit

Permalink
Merge pull request #212 from vuejs/issue-210-is-visible
Browse files Browse the repository at this point in the history
Add isVisible and stub transition/transition-group by default
  • Loading branch information
lmiller1990 committed Sep 26, 2020
2 parents 3110633 + eeb74c3 commit db9fb9a
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 10 deletions.
11 changes: 8 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComponentPublicInstance } from 'vue'
import { GlobalMountOptions } from './types'
import { VueWrapper } from './vueWrapper'
import { ComponentPublicInstance } from 'vue'

interface GlobalConfigOptions {
global: GlobalMountOptions
Expand All @@ -19,7 +19,7 @@ interface Plugin {
}

class Pluggable {
installedPlugins = [] as Array<Plugin>
installedPlugins: Plugin[] = []

install(
handler: (
Expand Down Expand Up @@ -55,7 +55,12 @@ class Pluggable {
}

export const config: GlobalConfigOptions = {
global: {},
global: {
stubs: {
transition: true,
'transition-group': true
}
},
plugins: {
VueWrapper: new Pluggable(),
DOMWrapper: new Pluggable()
Expand Down
5 changes: 5 additions & 0 deletions src/domWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { nextTick } from 'vue'

import { createWrapperError } from './errorWrapper'
import { TriggerOptions, createDOMEvent } from './createDomEvent'
import { isElementVisible } from './utils/isElementVisible'

export class DOMWrapper<ElementType extends Element> {
element: ElementType
Expand Down Expand Up @@ -36,6 +37,10 @@ export class DOMWrapper<ElementType extends Element> {
return true
}

isVisible() {
return isElementVisible(this.element)
}

text() {
return this.element.textContent?.trim()
}
Expand Down
8 changes: 3 additions & 5 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,11 +350,9 @@ export function mount(
app.mixin(attachEmitListener())

// stubs
if (global.stubs || options?.shallow) {
stubComponents(global.stubs, options?.shallow)
} else {
transformVNodeArgs()
}
// even if we are using `mount`, we will still
// stub out Transition and Transition Group by default.
stubComponents(global.stubs, options?.shallow)

// mount the app!
const vm = app.mount(el)
Expand Down
35 changes: 35 additions & 0 deletions src/stubs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
transformVNodeArgs,
Transition,
TransitionGroup,
h,
ComponentPublicInstance,
Slots,
Expand Down Expand Up @@ -34,6 +36,17 @@ const createStub = ({ name, props }: StubOptions): ComponentOptions => {
return defineComponent({ name: name || anonName, render, props })
}

const createTransitionStub = ({
name,
props
}: StubOptions): ComponentOptions => {
const render = (ctx: ComponentPublicInstance) => {
return h(name, {}, ctx.$slots)
}

return defineComponent({ name, render, props })
}

const resolveComponentStubByName = (
componentName: string,
stubs: Record<any, any>
Expand Down Expand Up @@ -78,6 +91,28 @@ export function stubComponents(
transformVNodeArgs((args, instance: ComponentInternalInstance | null) => {
const [nodeType, props, children, patchFlag, dynamicProps] = args
const type = nodeType as VNodeTypes

// stub transition by default via config.global.stubs
if (type === Transition && stubs['transition']) {
return [
createTransitionStub({ name: 'transition-stub', props: undefined }),
undefined,
children
]
}

// stub transition-group by default via config.global.stubs
if (type === TransitionGroup && stubs['transition-group']) {
return [
createTransitionStub({
name: 'transition-group-stub',
props: undefined
}),
undefined,
children
]
}

// args[0] can either be:
// 1. a HTML tag (div, span...)
// 2. An object of component options, such as { name: 'foo', render: [Function], props: {...} }
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type GlobalMountOptions = {
components?: Record<string, Component | object>
directives?: Record<string, Directive>
stubs?: Record<any, any>
renderStubDefaultSlot?: boolean
}

export interface VueWrapperMeta {
Expand Down
36 changes: 36 additions & 0 deletions src/utils/isElementVisible.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*!
* isElementVisible
* Adapted from https://github.com/testing-library/jest-dom
* Licensed under the MIT License.
*/

function isStyleVisible<T extends Element>(element: T) {
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
return false
}

const { display, visibility, opacity } = element.style

return (
display !== 'none' &&
visibility !== 'hidden' &&
visibility !== 'collapse' &&
opacity !== '0'
)
}

function isAttributeVisible<T extends Element>(element: T) {
return (
!element.hasAttribute('hidden') &&
(element.nodeName === 'DETAILS' ? element.hasAttribute('open') : true)
)
}

export function isElementVisible<T extends Element>(element: T) {
return (
element.nodeName !== '#comment' &&
isStyleVisible(element) &&
isAttributeVisible(element) &&
(!element.parentElement || isElementVisible(element.parentElement))
)
}
118 changes: 118 additions & 0 deletions tests/isVisible.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { mount } from '../src'

describe('isVisible', () => {
const Comp = {
template: `<div><span v-show="show" /></div>`,
props: {
show: {
type: Boolean
}
}
}

it('returns false when element hidden via v-show', () => {
const wrapper = mount(Comp, {
props: {
show: false
}
})

expect(wrapper.find('span').isVisible()).toBe(false)
})

it('returns true when element is visible via v-show', () => {
const wrapper = mount(Comp, {
props: {
show: true
}
})

expect(wrapper.find('span').isVisible()).toBe(true)
})

it('returns false when element parent is invisible via v-show', () => {
const Comp = {
template: `<div v-show="false"><span /></div>`
}
const wrapper = mount(Comp)

expect(wrapper.find('span').isVisible()).toBe(false)
})

it('element becomes hidden reactively', async () => {
const Comp = {
template: `<button @click="show = false" /><span v-show="show" />`,
data() {
return {
show: true
}
}
}
const wrapper = mount(Comp)

expect(wrapper.find('span').isVisible()).toBe(true)
await wrapper.find('button').trigger('click')
expect(wrapper.find('span').isVisible()).toBe(false)
})

it('handles transitions', async () => {
const Comp = {
template: `
<button @click="show = false" />
<transition name="fade">
<span class="item" v-show="show">
Content
</span>
</transition>
`,
data() {
return {
show: true
}
}
}
const wrapper = mount(Comp, {})

expect(wrapper.find('span').isVisible()).toBe(true)
await wrapper.find('button').trigger('click')
expect(wrapper.find('span').isVisible()).toBe(false)
})

it('handles transition-group', async () => {
const Comp = {
template: `
<div id="list-demo">
<button @click="add" id="add">Add</button>
<button @click="remove" id="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" :key="item" class="list-item">
Item: {{ item }}
</span>
</transition-group>
</div>
`,
methods: {
add() {
this.items.push(2)
},
remove() {
this.items.splice(1) // back to [1]
}
},
data() {
return {
items: [1]
}
}
}
const wrapper = mount(Comp)

expect(wrapper.html()).toContain('Item: 1')
await wrapper.find('#add').trigger('click')
expect(wrapper.html()).toContain('Item: 1')
expect(wrapper.html()).toContain('Item: 2')
await wrapper.find('#remove').trigger('click')
expect(wrapper.html()).toContain('Item: 1')
expect(wrapper.html()).not.toContain('Item: 2')
})
})
58 changes: 56 additions & 2 deletions tests/mountingOptions/stubs.global.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import ComponentWithoutName from '../components/ComponentWithoutName.vue'
import ComponentWithSlots from '../components/ComponentWithSlots.vue'

describe('mounting options: stubs', () => {
let configStubsSave = config.global.stubs
beforeEach(() => {
config.global.stubs = {}
config.global.stubs = configStubsSave
})

afterEach(() => {
config.global.stubs = {}
config.global.stubs = configStubsSave
})

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

it('stubs transition by default', () => {
const Comp = {
template: `<transition><div id="content" /></transition>`
}
const wrapper = mount(Comp)

expect(wrapper.html()).toBe(
'<transition-stub><div id="content"></div></transition-stub>'
)
})

it('opts out of stubbing transition by default', () => {
const Comp = {
template: `<transition><div id="content" /></transition>`
}
const wrapper = mount(Comp, {
global: {
stubs: {
transition: false
}
}
})

// Vue removes <transition> at run-time and does it's magic, so <transition> should not
// appear in the html when it isn't stubbed.
expect(wrapper.html()).toBe('<div id="content"></div>')
})

it('opts out of stubbing transition-group by default', () => {
const Comp = {
template: `<transition-group><div key="content" id="content" /></transition-group>`
}
const wrapper = mount(Comp, {
global: {
stubs: {
'transition-group': false
}
}
})

// Vue removes <transition-group> at run-time and does it's magic, so <transition-group> should not
// appear in the html when it isn't stubbed.
expect(wrapper.html()).toBe('<div id="content"></div>')
})

it('stubs transition-group by default', () => {
const Comp = {
template: `<transition-group><div key="a" id="content" /></transition-group>`
}
const wrapper = mount(Comp)
expect(wrapper.find('#content').exists()).toBe(true)
})

describe('stub slots', () => {
const Component = {
name: 'Parent',
Expand Down

0 comments on commit db9fb9a

Please sign in to comment.